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

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.

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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0007 Β· 15 files
FileTypeDescriptionSize
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
README.md readme build/run/expected + leak-detection logic
↓ download 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.

VERDICT.md verdict full narrative: mechanism, reproduction, fix, Phase 8 validation
↓ download raw

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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline pad=0xfffff800 8/8; patched pad=0x00000000 0/8.

BEFORE: 0xfffff800. AFTER: 0x00000000.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Tue Jul 14 18:37:19 UTC 2026

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.