# DF-3055 — dirfs_nsymlink/dirfs_nmkdir execute the SUCCESS path when dirfs_alloc_file() fails: dirfs_knote(NULL) crash, negative-cache of an existing name, error clobbered by stale errno

## Verdict
**REPRODUCED** (deterministic harness, 3/3 runs identical) — when
`dirfs_alloc_file()` fails after the host `symlink()`/`mkdirat()` succeeded,
`dirfs_nsymlink` still executes its success block:

```c
:1194  error = dirfs_alloc_file(dmp, &dnp, pdnp, ncp, vpp, NULL, 0);
:1196  if (error)
:1197          error = errno;              /* real error clobbered by STALE errno */
:1198  cache_setunresolved(ap->a_nch);
:1199  cache_setvp(ap->a_nch, *vpp);       /* *vpp == NULL -> negative entry for an EXISTING name */
:1200  dirfs_knote(*vpp, NOTE_WRITE);      /* NULL deref -> KNOTE(&NULL->v_pollinfo...) */
```

`kern_symlink()` initializes `vp = NULL` before VOP_NSYMLINK and
`dirfs_alloc_file` only assigns `*vpp` on success (subr.c:212-213), so `*vpp`
is NULL on every failure path (openat failure subr.c:196-199, stat failure
subr.c:202-210). `dirfs_knote()` computes `&vp->v_pollinfo.vpi_kqinfo.ki_note`
and KNOTE (sys/sys/event.h:168) dereferences the list head — a NULL-pointer
dereference (SIGSEGV/panic). The harness proves it with a two-TU transcription
(the KNOTE machinery in a separate translation unit, mirroring
dirfs_vnops.c vs kern_event.c; a single TU lets gcc8 fold the NULL away).

`dirfs_nmkdir` (vnops.c:1067-1073) has the same structure but its knote is on
`dvp` (safe); it still executes `cache_setvp(ap->a_nch, NULL)` at :1072 and
clobbers the error at :1070 — the kernel namecache negative-caches a name
whose object EXISTS on the host, so subsequent lookups return ENOENT until
the entry is invalidated, and mkdir reports a wrong (or 0 = success) error.

## The errno clobber (proven)

`:1196-1197` / `:1070` replace the real `dirfs_alloc_file` error with the
vkernel process's libc `errno` — which the LAST libc call did not set (the
create succeeded; the failure was inside kern-level code). Harness output:

```
real alloc_file error=2 (ENOENT), stale errno=0  -> nsymlink returns 0 (SUCCESS despite failure!)
real alloc_file error=2 (ENOENT), stale errno=13 -> nsymlink returns 13
real alloc_file error=2 (ENOENT), stale errno=21 -> nsymlink returns 21
```

A stale errno of 0 turns a failed symlink() into a SUCCESS return with
nothing created and a negative-cache entry — userland believes the symlink
exists.

## Reachability of the alloc_file failure

After `symlink(ap->a_target, path)` / `mkdirat()` succeed, alloc_file fails
when its `fstatat` (dirfs_node_stat) fails — practically when the fresh
object disappears in between, e.g. a concurrent same-uid process (another
vkernel process, or the vkernel's host user outside the vkernel) unlinking in
a loop; the window is findfd+fstatat but standard create/remove races win it.
(The over-length-path case crashes earlier — that is DF-3054.)

Impact ceiling: vkernel crash (DoS) via the racy trigger + persistent
namecache inconsistency; availability only.

## Distinction from known findings

DF-0856 covers alloc_file's node/refcount LEAK on the openat error path —
this finding is about what dirfs_vnops.c does AFTER the failure return
(NULL deref / negative cache / errno clobber), not the leak.

## Reproduction

```
ssh dfbsd-maxx
cd poc/DF-3055
./build.sh    # cc -O2 -Wall -o harness harness.c knote.c
./run.sh      # errno-clobber table + SIGSEGV in vulnerable variant; fixed variant survives
```

`knote.c` models the KNOTE/knote machinery with a volatile list head (gcc8
otherwise deletes the NULL deref as dead/UB code — documented in the harness
comments; several iterations were needed to defeat gcc8's IPA
constant-propagation + UB-DCE, see `poc_changes` in verdict.json).

Result (identical over 3 runs):
```
[nsymlink-vuln] child killed by SIGSEGV -- NULL DEREF at dirfs_knote(*vpp) CONFIRMED
[nsymlink-fixed] returned error=2        (real ENOENT propagated, no crash)
```

## Fix validation

`fix.diff` gates the success blocks on `error == 0` and drops the errno
clobber (propagate the real error): `git apply --check` RC=0 on the local tree
and guest /usr/src; compile-neutral (identical first compiler error patched
vs unpatched in the vkernel64 env — fix_*.log); harness FIXED variant
validates the behavior. Live boot: not_testable (dirfs vkernel-only).
