ktrsyscall copies the unfetched argument tail of sysmsg.extargs into the trace file: unprivileged self-tracing leaks stale kernel-stack words (groom-echo + kernel-canonical pointers) from failed stack-argument fetches
| Field | Value |
|---|---|
| ID | DF-2867 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N |
| CWE | CWE-908 / CWE-200 |
| File | sys/kern/kern_ktrace.c |
| Lines | 111-137 (fetch sites trap.c:1236-1253/:1415-1436, vkernel64 :1075-1094) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | kernleak |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
The ktrsyscall writer blindly copies all narg argument words; the
x86_64 trap paths hand it sysmsg.extargs with an UNINITIALIZED TAIL
when the user-stack argument fetch fails (7-arg syscalls: mmap,
sendfile, mountctl, vmspace_mmap; indirect syscall() path leaks 2
words). An unprivileged user tracing their own process (ktrcanset
self-grant) gets a repeatable kernel-stack oracle: fault records
verbatim echo a groom value that was never fetched (mechanism proof) and
in a loop 391β393/404 samples are kernel-canonical pointers varying per
boot. Repeatable 3/3 runs; no privileges; no crash. DF-0101/0102 cover
other uninit bytes in the same record family β different root cause
(fetch-error path) and much larger disclosure.
Proof of contest
VERIFIED (findings/poc/DF-2867/ktrleak.c, unpriv uid 1001, stock INVARIANTS): groom 0xaaaa0000deadaa7f echoed by the faulting record that never fetched arg7; totals 404/404 nonzero with 391β393 kernel-canonical across 3 runs; RESULT: LEAK REPRODUCED. Fix (bzero of the unfetched extargs tail at the three fetch sites) validated on a rebuilt kernel #1: fault records arg7=0, groom_echoed=0, kernel-canonical=0, exit NOT-reproduced.
Recommended fix
Validated fix.diff in findings/poc/DF-2867/.
Timeline
- 2026-09-02 Discovered during pass-2 audit of kern_ktrace.c (GLM 5.3); unpriv leak reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2867 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| ktrleak.c | β | 7.0 KB | view raw | |
| build.sh | β | 132 B | view raw | |
| run.sh | β | 331 B | view raw | |
| build.log | β | 8 B | view raw | |
| build.fail.log | β | 1.3 KB | view raw | |
| run1.log.txt | β | 20.8 KB | view raw | |
| run2.log.txt | β | 20.8 KB | view raw | |
| run3.log.txt | β | 20.9 KB | view raw | |
| leak_sample.txt | β | 828 B | view raw | |
| env.txt | β | 245 B | view raw | |
| fix.diff | β | 1.5 KB | view raw | |
| fix/build_reboot.log | β | 1016 B | view raw | |
| fix/patched_run1.log | β | 425 B | view raw | |
| fix/patched_run2.log | β | 425 B | view raw | |
| fix/patched_run3.log | β | 425 B | view raw | |
| VERDICT.md | β | 4.5 KB | β raw | |
| verdict.json | β | 5.3 KB | view raw |
DF-2867 β VERDICT
Finding
ktrsyscall() (sys/kern/kern_ktrace.c:111-137) writes all narg
argument words it is handed into the KTR_SYSCALL trace record. On the
x86_64 syscall paths, when a syscall with more arguments than the
register-passing ABI (regcnt) is invoked and the copyin() of the
stack-passed remainder fails, the kernel still calls ktrsyscall()
with the full narg β but only regcnt words of sysmsg.extargs were
initialized. The unfetched tail is stale kernel stack, and it is
appended verbatim to the user-readable trace file.
Sites (all reachable from an unprivileged process):
- sys/platform/pc64/x86_64/trap.c:1240-1252 (syscall2, regcnt=6, leaks
extargs[6] = 1 word for a 7-arg syscall)
- sys/platform/pc64/x86_64/trap.c:1419-1435 (sys_xsyscall indirect
path, regcnt=5, leaks extargs[5..6] = 2 words for a 7-arg syscall)
- sys/platform/vkernel64/x86_64/trap.c:1079-1093 (vkernel64 syscall2,
same as pc64)
(the vkernel64 indirect site at trap.c:1246-1253 returns before
ktrsyscall and does not leak)
7-arg syscalls exist: mmap (197), sendfile (393), mountctl (468),
vmspace_mmap (489) β AS(xxx_args)==7 in sys/kern/init_sysent.c;
sys/sys/sysproto.h:530-538 (mmap_args).
Reproduction (baseline β stock kernel)
Guest: DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC (Thu Jul 2 06:02:54
UTC 2026), user maxx (uid 1001, unprivileged).
PoC: ktrleak.c
1. ktrace(2) (SYS_ktrace=45) on own pid with KTRFAC_SYSCALL β
allowed by ktrcanset() (self, no P_SUGID, no KTRFAC_ROOT).
2. Phase A (mechanism): a valid raw mmap whose arg7 is fetched from
a mapped stack slot grooms extargs[6]=0xAAAA0000DEADAA7F; then a
raw mmap with RSP inside a PROT_NONE page β copyin(arg7) fails
(EFAULT), yet a KTR_SYSCALL record with narg=7 is emitted whose
arg7 equals the groom value that was never fetched.
3. Phase B (kernel data): faulting mmaps in a loop with user-mode
delays; timer/trap residue repopulates the stack region holding
syscall2's sysmsg; the records capture it.
Results (full logs: run1/2/3.log.txt):
- Phase A echo confirmed every run: fault records repeat the groom value.
- Phase B: 391/392/393 of 404 samples are kernel-canonical
(0xffff...) kernel-stack/heap pointers, e.g.
0xfffff80117e579e0, 0xfffff8008f842dc0 (both above kernel
_end=0xffffffff81b25e40 β KVA arena / kernel-stack addresses;
value varies across boots).
- Deterministic, repeatable, no privileges, no crash; the user simply
reads their own trace file afterwards.
Status: reproduced / impact=leak / confidence=certain.
Fix validation
fix.diff zeroes extargs[regcnt..narg) immediately after the
register-arg bcopy at all three sites, so a failed user-stack fetch
leaves zeros instead of stale stack (6 insertions, git-apply-able,
verified git apply --check against the read-only tree).
- Guest reverted via
vm.sh reset with-src, patch applied to the guest's/usr/src, kernel rebuilt withmake -j6 nativekernel KERNCONF=X86_64_GENERIC(rc=0, -Werror), installed (make installkernel, rc=0) and booted as#1: Wed Sep 2 15:21:48 UTC 2026. - Re-ran the exact PoC 3x on the patched kernel (
fix/patched_run*.log): every fault record now carriesarg7=0x0000000000000000(no groom echo β records [001]/[003] are zero instead of the groom value),summary: total=404 nonzero=2 groom_echoed=0 kernel-canonical=0,RESULT: NOT reproduced, exit 1. Contrast with baseline on stock #0:nonzero=404 groom_echoed=11-13 kernel-canonical=391-393, exit 0.
fix_status: fixed (baseline reproduced=1, patched reproduced=0).
Why this matters
ktrace(2) is available to every unprivileged user for their own
processes (the standard ktrcanset self-grant). This turns a routine
ktrace -i/debugging feature into a repeatable kernel stack oracle:
the attacker chooses which syscall fails and when, sampling stale stack
contents at a fixed frame offset. Disclosed material includes kernel
text/data/heap/stack pointers (defeats any kernel address-space
randomization) and whatever residue prior kernel paths (traps,
interrupts, copyins) left at that depth.
Related (not re-reported)
DF-0101 (ktr_header ktr_buf pointer + padding leak) and DF-0102 (ktr_sysret.ktr_eosys / ktr_syscall struct padding) cover other uninitialized bytes in the same record family; this finding is the much larger disclosure channel (full stale kernel-stack words via the argument tail) and a different root cause (trap.c fetch-error path).
Fix verification
fixedApplied fix.diff in guest /usr/src, make -j6 nativekernel KERNCONF=X86_64_GENERIC rc=0 (-Werror), make installkernel rc=0, rebooted into kernel #1 (Sep 2 15:21:48). Exact PoC re-run 3x: faulting records now carry arg7=0 (no groom echo), nonzero=2 (only legit groom records), kernel-canonical=0, RESULT: NOT reproduced, exit 1. Baseline (stock #0) simultaneously shows 404/404 nonzero with 391-393 kernel-canonical and groom echo. Leak eliminated by the fix.
['fix/patched_run1.log', 'fix/patched_run2.log', 'fix/patched_run3.log', 'fix/build_reboot.log', 'run1.log.txt (baseline contrast)']
Confirmed kernel references
Detail
Exploit chain
unpriv user -> ktrace(2) on own pid (ktrcanset self-grant, kern_ktrace.c:668-684) -> raw mmap (SYS_mmap=197, 7 args, mmap_args sysproto.h:530) with arg7 fetched from unmapped user stack -> trap.c:1243 copyin fails -> trap.c:1248-1250 ktrsyscall(lp,197,7,&sysmsg.extargs) with extargs[6] uninitialized -> kern_ktrace.c:131-132 copies it into ktr_syscall.ktr_args[6] -> ktrwrite() appends to /tmp/df2867.trace owned by the attacker -> attacker reads stale kernel stack (kernel pointers) at will; indirect path (regcnt=5) leaks 2 words.
Evidence (decisive lines)
['run1.log.txt lines [000]-[003]: groom 0xaaaa0000deadaa7f echoed by the faulting record [001] that never fetched arg7; [013]+ 0xfffff80117e579e0 kernel-canonical', 'run1/2/3.log.txt summary: total=404 nonzero=404 groom_echoed=11-13 kernel-canonical=391/392/393, RESULT: LEAK REPRODUCED', 'fix/patched_run1-3.log: fault records arg7=0x0000000000000000, summary nonzero=2 groom_echoed=0 kernel-canonical=0, RESULT: NOT reproduced', 'fix/build_reboot.log: nativekernel RC=0, installkernel completed Wed Sep 2 15:32:18 UTC 2026, booted #1 Wed Sep 2 15:21:48 UTC 2026, bzero fix present at /usr/src trap.c:1242,1423 + vkernel64 trap.c:1081']
PoC changes
No prior seed (first pack for this finding). Fixed during bring-up: (1) added sys/param.h + sys/mman.h includes for MAXCOMLEN/mmap constants; (2) parser arg offset 4 -> 8 (register_t 8-byte alignment inside struct ktr_syscall pads 4 bytes after the two shorts); (3) after fix-validation exposed an exit-code ambiguity, groom-echo counting tightened to exclude the two legit groom records (samples[0]/[2]).
Verified recommended fix
bzero the unfetched tail of sysmsg.extargs (argsdst[regcnt..narg)) right after the register-arg bcopy at all three syscall-arg fetch sites (pc64 syscall2 + sys_xsyscall, vkernel64 syscall2) before attempting copyin - see fix.diff.
Verdict
On the stock X86_64_GENERIC kernel an unprivileged user (uid 1001) ktrace(2)s their own process with KTRFAC_SYSCALL and issues raw 7-arg mmap(2) syscalls with the stack-passed 7th argument in a PROT_NONE page. syscall2()/sys_xsyscall() copy only regcnt words into sysmsg.extargs, fail the copyin of the remainder, and STILL call ktrsyscall() with the full narg; kern_ktrace.c:127-132 copies all narg words into the KTR_SYSCALL record appended to the user-readable trace file. Fault records verbatim echo a groom value that was never fetched (mechanism proof) and, in a loop, 391-393/404 samples are kernel-canonical (0xffff...) kernel-stack/heap pointers varying per boot (e.g. 0xfffff80117e579e0, 0xfffff8008f842dc0, above kernel _end 0xffffffff81b25e40). Repeatable across 3/3 runs; no privileges; no crash. fix.diff (bzero of the unfetched extargs tail at pc64 direct+indirect and vkernel64 sites) was applied in-guest, rebuilt with make nativekernel (rc=0, -Werror), installed and booted: identical PoC then shows fault records arg7=0, groom_echoed=0, kernel-canonical=0, exit 1 (RESULT: NOT reproduced). Baseline vs patched fully logged.
No comments yet.