Uninitialized struct sigaction trailing padding leaked to userspace via oact copyout
| Field | Value |
|---|---|
| ID | DF-0007 |
| Status | new |
| Severity | Info |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N |
| CWE | CWE-908 Use of Uninitialized Resource |
| File | sys/kern/kern_sig.c |
| Lines | 384, 260-279, 397 |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-06-29 |
| Reported | pending |
Summary
On 64-bit (amd64) struct sigaction is 32 bytes with 4 bytes of trailing
alignment padding at offset 28-31. sys_sigaction() stack-allocates its
oact uninitialized; kern_sigaction() fills oact field-by-field (handler,
mask, flags) but never writes the trailing padding; copyout(oactp, uap->oact,
sizeof(oact)) then copies the full 32-byte struct to userspace β leaking up to
4 bytes of kernel-stack residue to an unprivileged caller of
sigaction(signo, NULL, &oact). This is a classic CWE-908 info-leak (a weak
KASLR/stack-residue oracle). i386 is unaffected (sizeof(struct sigaction)
== 24, no trailing pad).
Root cause
struct sigaction (sys/sys/signal.h:221-228) on amd64:
offset 0: union __sigaction_u { void (*)(int); void (*)(int,siginfo*,void*); } (8 bytes)
offset 8: int sa_flags (4 bytes)
offset 12: sigset_t sa_mask /* unsigned int __bits[_SIG_WORDS=4] */ (16 bytes, 4-byte aligned)
offset 28: <trailing alignment padding> (4 bytes) <-- NOT a named field
sizeof(struct sigaction) == 32 (struct alignment is 8)
sigset_t is unsigned int __bits[4] (sys/sys/_sigset.h:35-39, 16 bytes,
4-byte-aligned), so sa_mask sits directly after sa_flags with no internal
gap; the only uninitialized region is the 4 trailing padding bytes at
offset 28-31 that pad the struct to 8-byte alignment.
In sys_sigaction (sys/kern/kern_sig.c:384):
struct sigaction act, oact; /* uninitialized stack variables */
kern_sigaction (sys/kern/kern_sig.c:260-279) writes only the named
fields:
if (oact) {
oact->sa_handler = ps->ps_sigact[_SIG_IDX(sig)]; /* offset 0-7 */
oact->sa_mask = ps->ps_catchmask[_SIG_IDX(sig)];/* offset 12-27 */
oact->sa_flags = 0; /* offset 8-11 */
/* ... |= SA_* ... into sa_flags ... */
}
β it never touches offset 28-31. Then sys_sigaction does
(sys/kern/kern_sig.c:397):
error = copyout(oactp, uap->oact, sizeof(oact)); /* copies all 32 bytes */
which propagates the 4 uninitialized trailing bytes to userspace. On i386
the union is 4 bytes and sizeof(struct sigaction) == 24 (struct alignment 4),
so there is no trailing padding and the path is unaffected.
Threat model & preconditions
- Attacker position: any local unprivileged user.
- Privileges gained or impact: information disclosure. Each
sigaction(signo, NULL, &oact)call leaks up to 4 bytes of kernel stack whose contents depend on prior syscall residue (pointer fragments, etc.). Not a direct LPE, but a samplable KASLR/stack-residue oracle. - Required config or capabilities: none; default amd64 kernel.
- Reachability: the
sigaction(2)syscall, directly.
Proof of concept
PoC source: findings/poc/DF-0007/leak_sigaction.c
Prefills a userland struct sigaction with a marker (0xAA), calls
sigaction(SIGUSR1, NULL, &oact), and inspects the trailing 4 bytes
(offset 28-31).
Build & run
cc -o leak_sigaction findings/poc/DF-0007/leak_sigaction.c ./leak_sigaction # as a non-root user, on amd64
Expected output
sizeof(struct sigaction) = 32 sample 0: padding bytes = 78 56 34 12 (word 0x12345678) ... samples with non-marker padding (leaked residue): N/8 result: LEAK CONFIRMED
The bytes vary (kernel-stack residue); what matters is they are neither the marker nor data the kernel wrote. On a fixed kernel the padding reads as the marker or zero.
Impact
Low-impact kernel-memory info leak (up to 4 bytes per call, stack residue). Valuable primarily as a hardening fix and as one ingredient in a larger info-leak/KASLR-defeat toolkit. Rated Info.
Recommended fix
Zero the stack-allocated oact (and act) before use, so the padding is
defined:
--- a/sys/kern/kern_sig.c
+++ b/sys/kern/kern_sig.c
@@ -381,7 +381,8 @@ int
sys_sigaction(struct sysmsg *sysmsg, const struct sigaction_args *uap)
{
- struct sigaction act, oact;
+ struct sigaction act = {};
+ struct sigaction oact = {};
struct sigaction *actp, *oactp;
int error;
A defense-in-depth alternative is to zero the trailing padding explicitly in
kern_sigaction before returning, but initializing the whole struct at the
syscall boundary is the minimal, idiomatic fix. (Equivalent discipline should
be applied to other field-by-field copyout paths in the file as they are
encountered.)
References
sys/kern/kern_sig.c:384β uninitializedoactdeclaration.sys/kern/kern_sig.c:260-279β field-by-field fill (no padding write).sys/kern/kern_sig.c:397βcopyout(sizeof(oact)).sys/sys/signal.h:221βstruct sigaction.sys/sys/_sigset.h:35βsigset_t=unsigned int[4](16 bytes).- CWE-908 Use of Uninitialized Resource; compare FreeBSD's padding-zeroing in
sigaction.
Timeline
- 2026-06-29 Discovered during automated file-by-file audit of
sys/kern/kern_sig.c. - pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0007 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| leak_sigaction.c | trigger-source | unpriv sigaction() padding-leak PoC; dumps full 32-byte struct + flags non-zero/non-marker residue | 4.7 KB | view raw |
| build.sh | build-script | cc -o leak_sigaction leak_sigaction.c | 186 B | view raw |
| run.sh | run-script | ./leak_sigaction (as unprivileged user) | 374 B | view raw |
| build.log | build-log | PoC build output (cc 8.3), exit 0 | 73 B | view raw |
| run.log | run-log | baseline run #1 on unpatched #0: padding 0xfffff800, 8/8 leaked, LEAK CONFIRMED | 1.3 KB | view raw |
| run.2.log | run-log | baseline run #2 on unpatched #0: identical 0xfffff800, 8/8 leaked | 1.3 KB | view raw |
| run.3.log | run-log | baseline run #3 on unpatched #0: identical 0xfffff800, 8/8 leaked | 1.3 KB | view raw |
| fix_run.log | run-log | run on single-fix kernel #1: padding 0x00000000, 0/8 leaked, no residue | 1.4 KB | view raw |
| fix_build.log | build-log | full nativekernel build of the single-fix kernel (MODULES_OVERRIDE=), rc=0 | 5.6 MB | β download |
| fix.diff | suggested-fix | git-apply-able: zero-init act/oact at sys_sigaction decl (sys/kern/kern_sig.c:384) | 332 B | view raw |
| env.txt | environment | uname, kern.version, cc version, kernel sha256, source line (patched #1 state) | 643 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, reproduction, fix, Phase 8 validation | 8.8 KB | β raw |
| README.md | readme | build/run/expected + leak-detection logic | 2.6 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0007 β PoC
leak_sigaction.c β unprivileged leak of up to 4 bytes of kernel stack via
the trailing padding of struct sigaction returned by sigaction().
The issue
On amd64 struct sigaction (sys/sys/signal.h:221) is 32 bytes with 4
bytes of trailing padding at offset 28-31:
offset 0: union __sigaction_u (8) -- written offset 8: int sa_flags (4) -- written offset 12: sigset_t sa_mask (16) -- written (full assignment) offset 28: <trailing padding> (4) -- NOT written <-- LEAK sizeof = 32 (struct alignment 8)
sys_sigaction() (sys/kern/kern_sig.c:384) stack-allocates oact
uninitialized; kern_sigaction() (:260-279) writes oact field-by-field
but never the trailing padding; copyout(oactp, uap->oact, sizeof(oact))
(:397) copies all 32 bytes β leaking the 4 uninitialized kernel-stack
bytes. i386 is unaffected (sizeof == 24, no trailing pad).
Build
cc -o leak_sigaction leak_sigaction.c
Run
As an unprivileged user on amd64:
./leak_sigaction
Expected output (bug present, e.g. unpatched 6.5-DEVELOPMENT #0)
sizeof(struct sigaction) = 32 sample: 00 00 00 00 00 00 00 00 00 00 00 00 ff fe fe ff ff ff ff ff ff ff ff ff ff ff ff ff | 00 f8 ff ff (pad word 0xfffff800) ... (8/8 samples) ... samples with non-marker non-zero padding (leaked residue): 8/8 result: LEAK CONFIRMED
The padding word (00 f8 ff ff = 0xfffff800) is a kernel-address-space
pointer fragment leaked from the kernel stack β it is neither the 0xAA
marker the buffer was filled with nor a value the kernel wrote to a named
field. The exact value is deterministic for this syscall path but is
genuinely uninitialized kernel-stack residue at offset 28-31.
Expected output (bug FIXED β single-fix kernel 6.5-DEVELOPMENT #1)
sample: 00 00 00 00 00 00 00 00 00 00 00 00 ff fe fe ff ff ff ff ff ff ff ff ff ff ff ff ff | 00 00 00 00 (pad word 0x00000000) ... samples with non-marker non-zero padding (leaked residue): 0/8 result: no residue observed (padding zero/defined)
The fix (fix.diff) zero-initializes the act/oact structs at the
sys_sigaction declaration, so the trailing padding is defined (zero) before
copyout. The named fields (offsets 0-27) are unchanged.
Leak-detection logic
A sample is counted as leaked residue iff the trailing 4 bytes are
neither the 0xAAAAAAAA marker (what the userland buffer was memset with)
nor 0x00000000 (zero = defined by the fix). On the buggy kernel the
padding holds non-zero kernel-stack residue; on the fixed kernel it is zero.
DF-0007 β Verification Verdict
| Field | Value |
|---|---|
| Verdict | REPRODUCED (info leak), then FIXED (validated on a built single-fix kernel) |
| Impact | leak:4bytes β up to 4 bytes of uninitialized kernel-stack residue per sigaction(SIGUSR1, NULL, &oact) call (Info / CWE-908) |
| Confidence | certain |
| Guest (unpatched baseline) | DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (x86_64, X86_64_GENERIC) |
| Guest (single-fix kernel) | DragonFly 6.5-DEVELOPMENT #1: Tue Jul 14 18:37:19 UTC 2026 |
1. The claim (and why it is correct)
On amd64, struct sigaction (sys/sys/signal.h:221-228) is laid out as:
offset 0: union __sigaction_u { void(*)(int); void(*)(int,siginfo*,void*); } (8 bytes)
offset 8: int sa_flags (4 bytes)
offset 12: sigset_t sa_mask /* unsigned int __bits[_SIG_WORDS=4] */ (16 bytes)
offset 28: <trailing alignment padding> (4 bytes) <-- NOT a named field
sizeof(struct sigaction) == 32 (struct alignment is 8)
sigset_t is unsigned int __bits[4] (sys/sys/_sigset.h:35-39, 16 bytes), so sa_mask
sits directly after sa_flags with no internal gap; the only uninitialized region is the
4 trailing padding bytes at offset 28-31.
In sys_sigaction (sys/kern/kern_sig.c:384):
struct sigaction act, oact; /* uninitialized stack variables */
kern_sigaction (sys/kern/kern_sig.c:260-279) writes only the named fields:
if (oact) {
oact->sa_handler = ps->ps_sigact[_SIG_IDX(sig)]; /* offset 0-7 */
oact->sa_mask = ps->ps_catchmask[_SIG_IDX(sig)];/* offset 12-27 */
oact->sa_flags = 0; /* offset 8-11 */
/* ... |= SA_* ... into sa_flags ... */
}
β it never writes offset 28-31. Then sys_sigaction does
(sys/kern/kern_sig.c:397):
error = copyout(oactp, uap->oact, sizeof(oact)); /* copies all 32 bytes */
which propagates the 4 uninitialized trailing bytes to userspace. The data flow was traced
line-by-line and matches the finding exactly. On i386 the union is 4 bytes and
sizeof(struct sigaction) == 24 (no trailing pad), so i386 is unaffected.
2. Reproduction on the unpatched #0 baseline
PoC leak_sigaction.c: memset's a userland buffer with the 0xAA marker, calls
sigaction(SIGUSR1, NULL, &oact), and inspects the trailing 4 bytes (offset 28-31) plus
the full 32-byte struct. Run as the unprivileged user maxx (uid 1001, not in wheel).
Decisive baseline output (3/3 identical runs):
sizeof(struct sigaction) = 32 layout: [0..7]=sa_handler [8..11]=sa_flags [12..27]=sa_mask [28..31]=<padding> sample: 00 00 00 00 00 00 00 00 00 00 00 00 ff fe fe ff ff ff ff ff ff ff ff ff ff ff ff ff | 00 f8 ff ff (pad word 0xfffff800) ... (8/8 samples identical) ... samples with non-marker non-zero padding (leaked residue): 8/8 result: LEAK CONFIRMED
Leaked value: 00 f8 ff ff = 0xfffff800. This is a kernel-address-space pointer
fragment (DragonFly maps kernel text/data in the high 0xfffff8xx... region). It is:
- not the 0xAAAAAAAA marker the userland buffer was filled with (so copyout overwrote
the user buffer entirely β as expected),
- not a value the kernel wrote to a named field (offsets 0-27: handler=SIG_DFL=0,
flags=0, mask=the catch-mask β all distinct from the padding),
- a non-zero residue from a kernel-stack slot the syscall path leaves at that offset.
The exact value is deterministic for this syscall path (the same stack frame is set up the
same way each call), which is normal for a stack-residue leak via a deterministic syscall β
the bytes are genuinely uninitialized kernel memory the kernel never wrote at offset 28-31.
4 bytes leaked per call, fully reproducible. impact = leak:4bytes.
3. Exploit chain
Not applicable β this is a pure read-only info leak (CWE-908). There is no write
primitive, no corruption, and therefore no escalation chain. The realistic impact ceiling is
a weak kernel-stack-residue / KASLR-assist oracle: an unprivileged local user can sample up
to 4 bytes of stack residue per sigaction() call. On this guest KASLR is already OFF, so
the leak is primarily a hardening defect; on a KASLR-enabled kernel it would be a (weak)
information ingredient.
4. PoC changes I made
The reviewer-supplied PoC compiled and ran first try, but its leak criterion
(padding != 0xAAAAAAAA marker) false-positives on a fixed kernel: the fix zeroes the
padding, so the fixed kernel reads 0x00000000, which is != marker and was incorrectly
counted as a leak. I sharpened it:
- Dumps the full 32 bytes per sample so the leak region (offset 28-31) is visually
distinct from the kernel-written fields (offsets 0-27).
- Corrected the leak criterion to padding != marker && padding != 0: leaked residue is
non-zero kernel-stack data; a zero padding is defined (the fix wrote it), not leaked.
The finding's own README states "On a fixed kernel the padding reads as the marker or
zero", so this matches the intended fixed behavior.
- Added a dirty_stack() helper (pipe/sysctl/getpid/getuid) between samples to maximize
visible residue.
Build/run are unchanged: cc -o leak_sigaction leak_sigaction.c then ./leak_sigaction as
an unprivileged user.
5. The fix (fix.diff)
Zero-initialize both act and oact at the sys_sigaction declaration so the trailing
padding is defined before kern_sigaction fills the named fields and copyout ships the
whole struct. Per C11 6.7.9 Β§21, { 0 } zero-fills the entire aggregate including padding
bytes.
--- a/sys/kern/kern_sig.c
+++ b/sys/kern/kern_sig.c
@@ -381,7 +381,7 @@
int
sys_sigaction(struct sysmsg *sysmsg, const struct sigaction_args *uap)
{
- struct sigaction act, oact;
+ struct sigaction act = { 0 }, oact = { 0 };
struct sigaction *actp, *oactp;
int error;
This matches the finding markdown's ## Recommended fix proposal (the finding used the
GNU = {} form; I used the standard-portable = { 0 } form, which gcc 8.3 accepts and
which C11 guarantees zero-fills padding). act is fully overwritten by copyin anyway, but
initializing it is harmless defense-in-depth and keeps the declaration symmetric.
6. Phase 8 β fix validation on a built single-fix kernel
8a. Baseline (unpatched #0, with-src snapshot): the PoC reproduces the leak
(0xfffff800, 8/8 samples, 3/3 runs) β the "before" half. (run.log, run.2.log, run.3.log.)
8b/8c. Apply + build: applied only fix.diff to /usr/src, built
make -j6 nativekernel KERNCONF=X86_64_GENERIC MODULES_OVERRIDE= (modules unchanged β the
fix is in the main kernel). Clean build, rc=0, kern_sig.o rebuilt (18:21 UTC).
(fix_build.log.)
8d. Install + reboot: the on-disk /boot/kernel/kernel carries the schg (system
immutable) flag, so a bare cp fails with EPERM and make installkernel fails on the
unbuilt modules (cam.ko: No such file); the correct install is
chflags noschg β cp kernel.stripped β chmod 555 β chflags schg, then sync; reboot.
After reboot kern.version bumped #0 β #1 (Tue Jul 14 18:37:19 UTC 2026) with sha256
d58a88a829940202e643cbf9d0d4fd18f8094492a03a0136aca7ba7a4c4df348. (One earlier attempt to
reboot after an unclean vm.sh down force-kill left the loader unable to read the kernel
("Unable to load /kernel/kernel"); recovered with vm.sh reset with-src and a clean
sync; reboot.)
8e. Re-run the PoC on the patched #1 kernel (3 runs, identical):
sample: 00 00 00 00 00 00 00 00 00 00 00 00 ff fe fe ff ff ff ff ff ff ff ff ff ff ff ff ff | 00 00 00 00 (pad word 0x00000000) ... (8/8 samples identical) ... samples with non-marker non-zero padding (leaked residue): 0/8 result: no residue observed (padding zero/defined)
The trailing padding is now 0x00000000 β zero-initialized by the fix β on every sample,
every run. The named fields (offsets 0-27) are byte-identical to the baseline (the fix does
not change kern_sigaction's field writes), confirming the fix only changed the padding.
8f. Classification: fixed. The leaked residue (0xfffff800) present on the unpatched
#0 baseline is gone (now 0x00000000) on the single-fix #1 kernel, deterministically,
across 3 runs β while the baseline still reproduces it across 3 runs. Clean before/after.
7. Conclusion
The finding is REPRODUCED as an Info-class info leak (4 bytes of kernel-stack residue per
call via the uninitialized trailing padding of struct sigaction), and the proposed fix
(zero-init the struct at the sys_sigaction boundary) is VALIDATED on a built and booted
single-fix kernel: the leak disappears deterministically. No escalation chain applies (pure
read-only leak). Guest reset to with-src (#0 unpatched baseline) at the end of the run.
Fix verification
fixedVALIDATED: baseline pad=0xfffff800 8/8; patched pad=0x00000000 0/8.
BEFORE: 0xfffff800. AFTER: 0x00000000.
Confirmed kernel references
Detail
Exploit chain
none -- read-only info leak. Ceiling: weak KASLR-assist. KASLR off on guest.
Evidence (decisive lines)
BEFORE: pad[28..31]=00 f8 ff ff (0xfffff800) 8/8. AFTER: pad=00 00 00 00 0/8.
PoC changes
Sharpened leak_sigaction.c (full 32B dump + corrected leak criterion + dirty_stack), fix.diff (zero-init act/oact = {0}), VERDICT.md, manifest.json.
Verified recommended fix
Change struct sigaction act, oact to act={0}, oact={0} at kern_sig.c:384. C11 zero-fills padding. Matches finding proposal (standard = {0} vs GNU = {}). Full diff in findings/poc/DF-0007/fix.diff.
Verdict
REPRODUCED. struct sigaction 32B with 4B trailing padding at [28..31]. sys_sigaction kern_sig.c:384 stack-alloc uninitialized, kern_sigaction writes only named fields, copyout ships 32B. Padding reads 0xfffff800 (KVA fragment) 8/8 samples.
No comments yet.