# DF-3056 — dirfs_nrename never updates dn_parent on cross-directory rename: all subsequent path-based ops act on the WRONG host file

## Verdict
**REPRODUCED** (deterministic harness with REAL syscalls on a real directory
tree, identical over 3 runs) — after a cross-directory rename,
`dirfs_nrename` updates only the node NAME:

```c
:984  if (error == 0) {
:985      vp = fncp->nc_vp;              /* file being renamed */
:986      dnp = VP_TO_NODE(vp);
:987      dirfs_node_setname(dnp, tncp->nc_name, tncp->nc_nlen);   /* name only */
           /* no dn_parent update for fdnp != tdnp */
```

Every host path dirfs builds later walks `dn_parent`:
`dirfs_findfd` (subr.c:470-481) and `dirfs_node_absolute_path[_plus]`
(subr.c:412-425). With a stale parent the constructed path points into the
OLD directory, so:

- `dirfs_getattr` (:389-393) stats whatever file NOW occupies the old path;
- the whole setattr family — chflags :477, chsize :495 (truncate),
  chown :521, chmod :545, chtimes :563 — via dirfs_node_absolute_path ->
  lchmod/lchown/lchflags/lutimes/truncate, operates on the old-path occupant;
- `dirfs_nremove` (:907-909) unlinkat's through dirfs_findfd — same stale path;
- fd-based I/O (dirfs_strategy pwrite/pread on dn_fd) keeps hitting the
  ORIGINAL inode → permanent split-brain (stat shows one file, read/write
  another);
- the passive-fd-list key (`dnp->dn_parent`,`dn_name`) used by dirfs_nresolve
  (:172-174) no longer matches, so the same host file can gain a second
  dirfs node.

Security consequence: the generic VFS layer checks permissions against the
VNODE's cached attributes (the file the user owns), while the host syscall
applies to the occupant of the old path and executes with the vkernel
process's uid. An unprivileged vkernel user can rename their own file out of
a shared directory, let another user's file take the old path, and then
chmod/chown/utimes/chflags/truncate/unlink THAT file (integrity break across
users; truncate destroys content). This is the DF-2979 "operating on the
wrong file" class.

## Reproduction (decision-logic transcription + real file ops)

The harness maintains the dirfs node graph exactly as dirfs builds it
(root(fd) -> dirA -> f, root -> dirB), performs the host rename the way
dirfs_nrename does (`rename(fpath, tpath)` with paths built by the transcribed
`dirfs_node_absolute_path`), applies the exact post-rename node update
(setname only), re-creates a 0600 "victim" at the old path, then transcribes
`dirfs_node_chmod` (lchmod on the path built from the node graph):

```
nrename: rename("/tmp/.../dirA/f", "/tmp/.../dirB/f")
post-rename node: name="f" parent="dirA"   (NOT updated)
victim created at OLD path /tmp/.../dirA/f (mode 0600)
user chmod 0777 on their moved file (vnode == dirB/f):
      lchmod("/tmp/.../dirA/f", 0777)   <-- STALE PATH
      stat(dirA/f) [the VICTIM]:  mode=0777  *** VICTIM MODIFIED (wrong-file op) ***
      stat(dirB/f) [the TARGET]:  mode=0644  (untouched — split-brain confirmed)
      getattr via stale node returns mode=0777 -> ls -l shows the VICTIM's attrs for dirB/f
FIXED (parent updated):
      lchmod("/tmp/.../dirB/f", 0644) -> correct file modified; victim left alone
```

Deterministic over 3 runs (run.log, run.2.log, run.3.log).

## Fix validation

`fix.diff` moves `dn_parent` (with refcount transfer) when fdnp != tdnp:
`git apply --check` RC=0 on the local tree and guest /usr/src;
compile-neutral (identical first compiler error patched vs unpatched);
harness FIXED variant proves the correct file is targeted. Live boot:
not_testable (dirfs vkernel-only).
