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

writepid() opens the -p pidfile without O_NOFOLLOW and truncates the symlink target with the launcher's privileges

Summary

The -p path opens O_RDWR|O_CREAT|O_EXLOCK without O_NOFOLLOW then ftruncate(0)+write(pid): a symlinked path destroys the TARGET file - and writepid runs before init_sys_memory so the clobber fires even on invocations that immediately error out. Reproduced: MARKER target became '[14255]'. Confused deputy against root-launched vkernels whose -p flows from lower-trust config (classic base-system pidfile clobber). O_NOFOLLOW fix validated (ELOOP, marker intact).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5775 Β· 9 files
FileTypeDescriptionSize
README.md β€” 2.3 KB ↓ raw
VERDICT.md β€” 1.9 KB ↓ raw
run5775.sh β€” 1.3 KB view raw
baseline_run.log β€” 348 B view raw
fixed_run.log β€” 364 B view raw
fix.diff β€” 1.5 KB view raw
env.txt β€” 293 B view raw
verdict.json β€” 2.4 KB view raw
manifest.json β€” 638 B view raw

DF-5775 β€” writepid() follows symlinks and truncates the -p target with the launcher's privileges

File: sys/platform/vkernel64/platform/init.c Lines: 1388-1401 (writepid) Severity: Low (arbitrary-file truncate/overwrite via symlinked pidfile, confused-deputy against root-launched vkernels) Status: reproduced (stock) / fix validated.

Reproduce (guest, root)

cd /usr/obj/usr/src/sys/VKERNEL64
echo "DF5775-MARKER-SECRET-ORIGINAL-CONTENT-MUST-SURVIVE" > /tmp/df5775_target
ln -sf /tmp/df5775_target /tmp/df5775_link
./kernel.debug -p /tmp/df5775_link        # writepid runs before -m validation
cat /tmp/df5775_target

Expected output

Vulnerable (baseline_run.log):

before: target=DF5775-MARKER-SECRET-ORIGINAL-CONTENT-MUST-SURVIVE
after:  target=[14255]                     <- pid number only
RESULT: target CLOBBERED via symlink (vulnerable)
kernel.debug: System memory size must be specified with -m

The symlink itself survives (ls -la); the TARGET was truncated to 0 (ftruncate, init.c:1400) and overwritten with the pid string.

Fixed (fixed_run.log, rebuilt kernel with fix.diff hunk 3):

after:  target=[DF5775-MARKER-SECRET-ORIGINAL-CONTENT-MUST-SURVIVE]
RESULT: target intact (not vulnerable)
Failed to create pidfile: Too many links     <- ELOOP from O_NOFOLLOW

Root cause / threat

open(pid_file, O_RDWR|O_CREAT|O_EXLOCK|O_NONBLOCK, 0666) (init.c:1390) has no O_NOFOLLOW: a symlinked -p path is followed, then ftruncate(fd, 0) + write(fd, pid) (init.c:1400-1401) destroy and replace the target's content. Production vkernels are launched by root (rc scripts, management wrappers) and the pidfile path is exactly the kind of field that flows from configuration; a root-launched vkernel fed -p /attacker/link -> /etc/anything truncates that file to 8 bytes of pid. Classic CWE-59 pidfile clobber, in base-system infrastructure.

Note: writepid() runs before init_sys_memory() (init.c:442-444), so the clobber needs no successful boot β€” a vkernel that immediately errors out still truncates the target.

Fix

fix.diff hunk 3: add O_NOFOLLOW (operator who genuinely wants a symlinked pidfile can still point the link at a dedicated file created first; the error is loud: "Failed to create pidfile: Too many links").

VERDICT.md
↓ download raw

DF-5775 β€” VERDICT

Status: reproduced (stock kernel, deterministic). Fix validated on a rebuilt kernel. Impact: arbitrary-file truncate/overwrite (content replaced with the pid) with the vkernel launcher's privileges β€” in the common root-launched deployment, root file destruction via a symlinked pidfile path. Severity Low (config-injection precondition), certain.

writepid() (init.c:1381-1404) opens the operator-supplied -p path with O_RDWR|O_CREAT|O_EXLOCK|O_NONBLOCK and no O_NOFOLLOW (init.c:1390), then ftruncate(fd, 0) (init.c:1400) and writes the pid (init.c:1401). A symlink at the pidfile path is followed: the TARGET is truncated and overwritten. Live proof on the guest (baseline_run.log):

before: target=DF5775-MARKER-SECRET-ORIGINAL-CONTENT-MUST-SURVIVE
after:  target=[14255]
RESULT: target CLOBBERED via symlink (vulnerable)

with the vkernel itself dying immediately afterwards ("System memory size must be specified with -m") β€” demonstrating that writepid runs BEFORE any memory/disk setup (init.c:442-444), so the clobber needs no bootable configuration at all.

Fixed kernel (fix.diff hunk 3: O_NOFOLLOW): open fails with ELOOP ("Failed to create pidfile: Too many links"), the vkernel exits, and the marker target is intact (fixed_run.log).

Reachability: -p is operator input; the security-relevant scenario is a privileged launcher consuming a pidfile path from lower-trust configuration (the same confused-deputy class as any root-run daemon with a configurable pidfile). No memory-safety primitive; hence Low.

Artifacts: run5775.sh, baseline_run.log, fixed_run.log, fix.diff.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline: target truncated to pid bytes. Patched: ELOOP ('Too many links'), vkernel exits, target intact.

baseline_run.log vs fixed_run.log
↓ fix.diffper-fix-DF-5775

Confirmed kernel references

Detail

Exploit chain

attacker controls a pidfile path flowing into a root-launched vkernel's -p (config injection) -> plants symlink at that path pointing at the victim file -> vkernel start truncates victim to 0 and writes an 8-byte pid -> arbitrary root file destruction (CWE-59)

Evidence (decisive lines)

baseline_run.log (marker -> [14255], 'CLOBBERED via symlink', link survives); fixed_run.log (marker intact, 'Failed to create pidfile: Too many links')

PoC changes

authored fresh (no seed); uses the no -m early-exit so the clobber is isolated from boot

Verified recommended fix

open the pidfile with O_NOFOLLOW added at init.c:1390

Verdict

writepid() opens the operator-supplied -p path without O_NOFOLLOW (init.c:1390) and then ftruncate(0)+write(pid) (init.c:1400-1401): a symlinked pidfile path lets the vkernel truncate and overwrite the symlink TARGET with the launching user's privileges (root for production vkernels). Reproduced live: marker file replaced by the bare pid via /tmp/df5775_link, with the vkernel failing immediately after (writepid precedes -m validation, so no bootable config is needed). Fixed kernel rejects the symlink with ELOOP and the target survives.