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)
PoC verification
Evidence pack
findings/poc/DF-2944 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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 persys/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
- Guest: DragonFly 6.5-DEVELOPMENT x86_64 (stock INVARIANTS kernel #0).
acct_leak.c(root):creata fresh file,syscall(51, path)to enable accounting (init_sysent.c:68β acct is syscall 51).- 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 childrenexecve("/tmp/acct_helper")(deep exec-path stack usage) which setuid(65534), churn,_exit(9). syscall(51, NULL)disables accounting.- 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-CONFIRMEDagain (50/50, pad1 49β50). - Raw file hexdump (
leak_sample.txt):ff ffvisible 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_testablefix.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
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.
No comments yet.