β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2944

acct_process() leaks 9 bytes of uninitialized kernel stack into every accounting record (struct acct alignment padding)

Field Value
ID DF-2944
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N
CWE CWE-457 / CWE-200
File sys/kern/kern_acct.c
Lines 198, 225-264, 276-278
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

acct_process() declares stack-local 'struct acct acct;', assigns every field, and vn_rdwr()s all sizeof(acct)=56 bytes to the accounting file without zeroing the struct. On x86_64, struct acct has 2 bytes of padding before the 8-byte-aligned ac_btime and 7 bytes of tail padding that no field covers. Each record written at every process exit therefore contains 9 bytes of the exiting thread's kernel-stack residue. Unprivileged users cannot enable accounting, but every exiting process β€” including unprivileged ones β€” emits a record whose 9 padding bytes are that thread's kernel-stack contents; whoever can read the admin-created accounting file (misconfigured perms, group/world-readable acct files, backups) harvests kernel-stack fragments β€” including kernel pointers β€” of arbitrary users' processes: a KASLR-defeat/ pointer-harvest channel and an information-hygiene defect in the audit trail. VERIFIED 3/3 runs: 49-50 of 50 records per run carried non-zero padding, including pointer-shaped KVA contents with natural variance. No escalation chain (leak class, 9 bytes/record). Fix: bzero the record before assembly.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of kern_acct.c (GLM 5.3); leak reproduced 3/3 with unpriv-origin payload.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2944 Β· 13 files
FileTypeDescriptionSize
acct_leak.c β€” 5.7 KB view raw
acct_helper.c β€” 1.1 KB view raw
build.sh β€” 160 B view raw
run.sh β€” 95 B view raw
build.log β€” 133 B view raw
run.log β€” 4.2 KB view raw
run.2.log β€” 572 B view raw
leak_sample.txt β€” 2.2 KB view raw
env.txt β€” 399 B view raw
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 4.3 KB ↓ raw
fix.diff β€” 598 B view raw
verdict.json β€” 3.4 KB view raw

DF-2944 β€” Uninitialized kernel-stack bytes leaked into process accounting records

File: sys/kern/kern_acct.c β€” acct_process() (lines 195–282) Class: CWE-457 (use of uninitialized memory) β†’ kernel-stack contents disclosure Severity: Low (acct(2) is root-only; exposure requires a readable accounting file) Verdict: REPRODUCED (3/3 runs) β€” LEAK-CONFIRMED

What is wrong

struct acct (sys/sys/acct.h:53) on x86_64 is 56 bytes and contains two runs of alignment padding that no field covers:

offset size content
22–23 2 pad before ac_btime (time_t needs 8-byte alignment)
49–55 7 tail pad after ac_flag (struct alignment 8 β‡’ sizeof 56)

acct_process() declares struct acct acct; on its kernel stack (kern_acct.c:198), assigns every field, and then writes the full 56 bytes to the accounting file with vn_rdwr(...) (kern_acct.c:276–278). The 9 padding bytes are never initialized, so each record written at every process exit contains residue of the exiting thread's kernel stack β€” i.e. fragments of whatever earlier syscalls left at those stack addresses (in the decisive run: 78 78 18 01 f8 ff ff β€” a pointer-shaped qword 0xfffff80118787800 in DragonFly's KVA range; across records mostly ff ff with variance).

Reproduce (in-guest, as root)

# push acct_leak.c + acct_helper.c to /root/poc/ (e.g. b64decode -r)
cc -O -o /tmp/acct_leak   /root/poc/acct_leak.c
cc -O -o /tmp/acct_helper /root/poc/acct_helper.c
rm -f /var/acct_leak.dat
/tmp/acct_leak

The harness: enables accounting on /var/acct_leak.dat via syscall(51, path); forks 25 children that setuid(65534), churn the kernel stack (30Γ— udp sendto to 127.0.0.1:0x5A5A + getsockname), and exit; then 25 children execve("/tmp/acct_helper") which do the same; disables accounting; parses all records and hexdumps bytes [22..24) and [49..56).

Expected output

=== 50 records, 50 with nonzero padding (pad1: 49, pad2: 1) ===
LEAK-CONFIRMED

with e.g. record 48 showing pad[49:56] = 78 78 18 01 f8 ff ff. Full logs: build.log, run.log, run.2.log (3 runs), raw bytes in leak_sample.txt (hexdump of the accounting file itself).

Success criterion

β‰₯1 record with non-zero padding bytes that (a) are not written by any field assignment in kern_acct.c and (b) vary across records/runs β€” demonstrating uninitialized kernel-stack disclosure into the file. Observed: 49–50/50 records per run, 3/3 runs.

Fix

bzero(&acct, sizeof(acct)) before filling the record β€” see fix.diff.

VERDICT.md
↓ download raw

DF-2944 VERDICT β€” REPRODUCED (leak)

Bottom line

acct_process() in sys/kern/kern_acct.c writes a stack-local struct acct to the accounting file without zeroing it first. On x86_64 the struct is 56 bytes and contains 9 bytes of alignment padding that no field assignment touches (2 bytes at offset 22–23 before the 8-byte-aligned ac_btime; 7 bytes of tail padding at 49–55). Every accounting record therefore carries 9 bytes of the exiting thread's kernel-stack residue. Verified live on the audit guest: 49–50 of 50 records per run carried non-zero padding, 3/3 runs, including pointer-shaped kernel-stack contents (78 78 18 01 f8 ff ff β†’ qword 0xfffff80118787800 at offset 48, DragonFly KVA range).

Root cause (path:line)

  • sys/kern/kern_acct.c:198 β€” struct acct acct; declared uninitialized.
  • sys/kern/kern_acct.c:225-264 β€” every field assigned; padding never written.
  • sys/kern/kern_acct.c:276-278 β€” vn_rdwr(UIO_WRITE, vp, (caddr_t)&acct, sizeof(acct), ...) writes all 56 bytes including the 9 uninit bytes.
  • Layout proof: sys/sys/acct.h:50-71 (comp_t=u16, time_t field at 24, dev_t=u32 per sys/sys/stat.h:61, u8 flag β‡’ 56-byte struct with pads). The PoC _Static_asserts the offsets against the guest's own headers and passed compilation (build.log).

How it was verified

  1. Guest: DragonFly 6.5-DEVELOPMENT x86_64 (stock INVARIANTS kernel #0).
  2. acct_leak.c (root): creat a fresh file, syscall(51, path) to enable accounting (init_sysent.c:68 β€” acct is syscall 51).
  3. Wave A: 25 forked children setuid(65534), churn the kernel stack with recognizable syscalls (30Γ— udp sendto 127.0.0.1:0x5A5A, 0xC3 payloads, getsockname), _exit(7). Wave B: 25 children execve("/tmp/acct_helper") (deep exec-path stack usage) which setuid(65534), churn, _exit(9).
  4. syscall(51, NULL) disables accounting.
  5. Parse the file in 56-byte records; dump bytes [22..24) and [49..56).

Observations (decisive)

  • Run 1: 50 records, 49 with non-zero padding (pad1: 49, pad2: 1); record 48 tail = 78 78 18 01 f8 ff ff (kernel pointer fragment); record 33 clean β€” showing natural variance, not a deterministic constant.
  • Runs 2 & 3: LEAK-CONFIRMED again (50/50, pad1 49–50).
  • Raw file hexdump (leak_sample.txt): ff ff visible at file offset 0x16–0x17 of every record; ac_uid=0xfffe (65534), ac_tty=0xffffffff (NOUDEV) confirm the field/padding framing is exactly as derived.
  • File size 2800 = 50 Γ— 56 βœ”.

Threat model / impact ceiling

  • Triggering is not privileged: any exiting process produces a record (the harness records carry ac_uid=65534). Enabling accounting and reading the file are privileged/permission-gated (acct(2) needs SYSCAP_NOACCT, kern_acct.c:128; the file is created by the admin β€” here mode 0600).
  • Ceiling: disclosure of kernel-stack fragments (potentially kernel pointers β€” demonstrated) of arbitrary users' processes to anyone who can read the accounting file. On systems without KASLR this is mostly an information-hygiene bug; on KASLR'd systems it is a pointer-harvest primitive for an attacker who can read the file (misconfigured perms, backup copies, group-readable accounting as shipped by some sites).
  • No integrity/availability impact: read-only disclosure, fixed 9 bytes/record.

Why not higher severity

Per the severity rubric this is "info leak of limited kernel memory [that] requires unusual config/privilege" β€” reading the accounting file requires privileges equivalent to the party that enabled accounting, in default configurations. Low severity, certain confidence (live-reproduced).

Exploit chain

None pursued beyond disclosure (leak class). Escalation would require a second primitive; the leaked 9 bytes/record are a harvesting channel, not a corruption primitive.

Fix

fix.diff β€” bzero(&acct, sizeof(acct)); before the field assignments in acct_process(). This zeroes all padding; every field is subsequently assigned, so record contents are unchanged. Fix-validation kernel rebuild was not performed (Low-severity, non-corruption finding β€” per audit contract, rebuild validation is mandated for memory-corruption findings); the one-line zeroing is provably sufficient by inspection of the assignment list at kern_acct.c:225-264.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

fix.diff authored post-verification (bzero of the record before field assignment zeroes both padding runs; all fields are subsequently overwritten so record semantics are unchanged). Patched-kernel rebuild not performed: Low-severity leak, not a memory-corruption finding, and the fix is provably sufficient by inspection; no fix_run.log/fix_build.log generated.

fix.diff; VERDICT.md 'Fix' section
↓ fix.diffper-fix-DF-2944

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (full 50-record dump; records 33 & 48 show variance incl. kernel-pointer residue in pad[49:56]); run.2.log (2 further runs, LEAK-CONFIRMED); leak_sample.txt (raw hexdump of /var/acct_leak.dat showing 'ff ff' at offset 0x16 of each 56-byte record and the 2800=50x56 framing); build.log (layout _Static_asserts passed); VERDICT.md for the full narrative

PoC changes

seed sketch rewritten entirely: raw syscall(51) for acct(2) (libc wrapper presence uncertain), _Static_assert proof of struct layout against guest headers, two churn waves (fork + execve) to vary kernel-stack residue, setuid(65534) children to prove unpriv-origin records, in-band b64decode file transfer (vm.sh run_root does not forward stdin)

Verified recommended fix

bzero(&acct, sizeof(acct)) at the top of acct_process() before filling the record (see fix.diff)

Verdict

acct_process() (sys/kern/kern_acct.c:195-282) fills a stack-local 'struct acct acct;' field-by-field and vn_rdwr()s all sizeof(acct)=56 bytes to the accounting file (kern_acct.c:276) without zeroing the struct. On x86_64 the struct has 9 bytes of alignment padding no field covers (offsets 22-23 before the 8-byte-aligned ac_btime, and 49-55 after ac_flag), so every accounting record written at every process exit discloses 9 bytes of the exiting thread's kernel-stack residue. Live-verified on the audit guest (DragonFly 6.5-DEVELOPMENT x86_64, stock kernel): 49-50 of 50 records per run carried non-zero padding across 3/3 runs, including pointer-shaped contents (tail pad '78 78 18 01 f8 ff ff' = qword 0xfffff80118787800 in DF KVA space) and natural variance across records. Records are generated by unprivileged exiting processes (records observed with ac_uid=65534), but exposure requires read access to the admin-created accounting file, hence Low severity.