# DF-0039 — Verdict

**REPRODUCED: NO**  ·  **Status: NOT REPRODUCED**  ·  **Confidence: certain**

> The TOCTOU exists in the *source* (`sys/kern/tty_pty.c:313-317`) but is
> **unreachable in the compiled master DEV kernel** because GCC 8.3 at `-O2`
> has CSE-fused the two `dev->si_drv1` reads into a single load, producing
> the same safe pattern that `ptcopen` uses. Empirically verified: 34
> million race attempts across 42 500 iterations produced **no panic**.

## What the finding claims (and the source really does show)

The C source for `ptsopen` (`sys/kern/tty_pty.c:313-317`) literally performs
two unlocked loads of `dev->si_drv1`:

```c
if (dev->si_drv1 == NULL)        /* :313  load #1 */
    return(ENXIO);
pti = dev->si_drv1;              /* :315  load #2 (claimed race window) */

lwkt_gettoken(&pti->pt_tty.t_token);   /* :317  NULL deref if pti==NULL */
```

`ptcclose()` → `pti_done()` nulls `dev->si_drv1` at `:279` *before*
`destroy_dev()` at `:281`, and the `pti` structure is never freed (`:293`),
so a single captured read would be stable.  The sibling `ptcopen`
(`:571-573`) is already written as a single read.  Everything the finding
says about the source is **correct**.

## Why it does not reproduce on this kernel (the disassembly proof)

`struct cdev::si_drv1` is at offset **0x98** (verified against
`sys/sys/conf.h:67-104` with `SPECNAMELEN=63`:
`si_flags(4)+pad(4)+si_inode(8)+si_uid(4)+si_gid(4)+si_perms(4)+pad(4)+link(16)+si_uminor(4)+si_umajor(4)+si_parent(8)+si_hash(16)+si_hlist(8)+si_name[64] = 152 = 0x98`).

Disassembling the actual shipped `/boot/kernel/kernel`
(`ffffffff806ba760 <ptsopen>`, DragonFly v6.5.0.1712.g89e6a-DEVELOPMENT,
built `Mon Jun 29 14:18:01 UTC 2026`) shows:

```asm
ffffffff806ba771:  4c 8b 77 08                mov    0x8(%rdi),%r14      ; r14 = ap->a_head.a_dev (the `dev` arg)
ffffffff806ba775:  4d 8b a6 98 00 00 00       mov    0x98(%r14),%r12     ; r12 = dev->si_drv1   <-- THE ONLY LOAD
ffffffff806ba77c:  4d 85 e4                   test   %r12,%r12           ; NULL check (the :313 test, on the value)
ffffffff806ba77f:  0f 84 ab 01 00 00          je     ffffffff806ba930   ; -> ENXIO return
ffffffff806ba785:  49 8d 44 24 28             lea    0x28(%r12),%rax    ; rax = &pti->pt_tty.t_token (reuses r12!)
...
ffffffff806ba794:  e8 27 e3 fb ff             callq  ffffffff80678ac0 <lwkt_gettoken>
```

The compiler has fused the `:313` check and the `:315` assignment into a
**single `mov 0x98(%r14),%r12`**, then reused `%r12` for the token address.
There is no second load.  Compare to `ptcopen` (`ffffffff806b91c0`), which
is already a single read in the source and compiles to the same shape:

```asm
ffffffff806b91d5:  49 8b 9d 98 00 00 00       mov    0x98(%r13),%rbx    ; rbx = dev->si_drv1   <-- single load
ffffffff806b91dc:  48 85 db                   test   %rbx,%rbx
ffffffff806b91df:  0f 84 db 01 00 00          je     ffffffff806b93c0
ffffffff806b91e5:  4c 8d 7b 28                lea    0x28(%rbx),%r15    ; reuses rbx
```

**Both functions are byte-for-byte the same safe pattern in the compiled
kernel.**  `pti_done` does still store NULL into `0x98(%rdi)` for the
slave's cdev (`movq $0x0,0x98(%rdi)` at `ffffffff806b8fc9`), but a store
can only race with a separate load — and there is no separate load in
`ptsopen`.  The race window the finding describes simply does not exist in
the generated code.

GCC is permitted to do this: in the C memory model, two reads of a
non-`volatile` lvalue with no intervening observable side-effect may be
coalesced (the compiler assumes no concurrent modification absent
`volatile`).  This is a perfectly legal optimization, and it is what masks
the bug.

## Empirical confirmation

`pts_race.c` (rewritten; the original used the non-existent `TIOCSPTLCK`
ioctl and `ptsname` without `<stdlib.h>` and did not build) races
`open(/dev/pts/N)` (×4 children, 200 opens each per cycle) against
`close(master)` in a tight loop, reachable as the unprivileged `maxx` user
(uid 1001, not in wheel).

60-second run as `maxx`:

```
[pts_race] iter=40500 (no panic)
[pts_race] iter=41000 (no panic)
[pts_race] iter=41500 (no panic)
[pts_race] iter=42000 (no panic)
[pts_race] iter=42500 (no panic)
```

42 500 iterations × 4 × 200 = **34 million ptsopen attempts**.  Guest still
up (`vm.sh status` ⇒ `up`), no `fatal trap` / `panic:` / `Stopped at` /
`db>` markers in `boot.log`.  See `run.log`, `ptsopen.kernel.asm`,
`ptcopen.kernel.asm`, `pti_done.kernel.asm`, `env.txt`.

## Classification

**(d) Genuinely not reachable on this kernel**, with a specific reason that
is *not* "the bug isn't real in source" but rather "the compiler
accidentally closes the race".  This is **fragile**: a future GCC upgrade,
a different `-O` level, LTO, or any code change that pokes a function call
or memory barrier between the two source lines would re-materialize the
two-load form and resurrect the race exactly as the finding describes.  It
is therefore worth fixing as defense-in-depth even though no current
exploit exists.

## Recommended fix (defense-in-depth)

Collapse the two reads into a single read into `pti` (matching `ptcopen`),
which makes the source match what the compiler is already generating.  See
`fix.diff` — a standalone `git apply`-able unified diff (passes
`git apply --check`).  This matches the finding markdown's `## Recommended
fix` proposal verbatim.

```diff
-	if (dev->si_drv1 == NULL)
-		return(ENXIO);
 	pti = dev->si_drv1;
+	if (pti == NULL)
+		return(ENXIO);
```

## Exploit chain

None.  No memory-corruption primitive is derivable: the race window that
would expose the NULL deref does not exist in the compiled kernel.

## PoC changes from the original

- Replaced the non-existent `ioctl(m, TIOCSPTLCK, 0)` (DragonFly has no
  `TIOCSPTLCK`; `unlockpt()` is the right libc helper) with `unlockpt(m)`.
- Added `<stdlib.h>` (for `ptsname` / `unlockpt`) and `<errno.h>`.
- Removed the leaky `for(;;)` opener children (process-table explosion);
  replaced with bounded bursts (`opener_child(buf, 200)`).
- Dropped the redundant `master_closer` thread (the closer thread in the
  original operated on *unrelated* masters and so could never race the
  slaves being opened).
- Added iteration logging so the negative result is observable.
- Added `ptsopen.kernel.asm` / `ptcopen.kernel.asm` / `pti_done.kernel.asm`
  as the decisive evidence that the compiled kernel has no second load.
