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)
PoC verification
Evidence pack
findings/poc/DF-5775 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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").
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
fixedBaseline: target truncated to pid bytes. Patched: ELOOP ('Too many links'), vkernel exits, target intact.
baseline_run.log vs fixed_run.log
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.
No comments yet.