# 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`):
```c
struct sigaction act, oact;          /* uninitialized stack variables */
```

`kern_sigaction` (`sys/kern/kern_sig.c:260-279`) writes only the **named** fields:
```c
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`):
```c
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.

```diff
--- 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.
