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

ctl DETACH is exempt from all authorization: any same-euid bystander can rip a victim out of an active tracer's session (clear P_TRACED, delete pending SIGTRAP, reparent, unstop); wakeup(curp) also never wakes a blocked CTL_WAIT tracer

Summary

procfs_control skips the entire credential gate for DETACH ('if (op != PROCFS_CTL_DETACH)', :122). The exemption's stated purpose (a tracer that lost permission may disengage) only justifies it for the current tracer, but it applies to anyone who can open the ctl file (any same-euid process, or a pre-opened fd across cred changes). A bystander's 'detach' clears P_TRACED, deletes the pending SIGTRAP, reparents to p_oppid, clears P_WAITED and unstops the victim - silently terminating a live (possibly root-run) debugging/monitoring session; the tracer's subsequent ctl ops fail EBUSY. Additionally wakeup((caddr_t)curp) at :223 wakes the detacher's pointer while a CTL_WAIT tracer sleeps on (caddr_t)p (:258), so third-party detaches never wake it. VERIFIED uid 1001: tracer T attaches victim V; bystander D writes 'detach' -> session destroyed, T's step fails EBUSY. Fix validated (DETACH restricted to current tracer per PT_DETACH + wakeup(p)): D gets EPERM, T's step succeeds. Same-uid kill(2) already permits coarser damage, hence Low.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3226 Β· 9 files
FileTypeDescriptionSize
df3226.c β€” 2.9 KB view raw
build.sh β€” 79 B view raw
run.sh β€” 89 B view raw
run.log β€” 267 B view raw
run.2.log β€” 267 B view raw
VERDICT.md β€” 2.8 KB ↓ raw
README.md β€” 2.7 KB ↓ raw
fix.diff β€” 794 B view raw
verdict.json β€” 3.4 KB view raw

DF-3226 β€” /proc/pid/ctl "detach" requires no authorization: bystanders can rip a tracee out of a live tracing session

File: sys/vfs/procfs/procfs_ctl.c:122-128 (authz exemption), :197-225 (DETACH body), :223 (wrong wakeup address) Severity: Low β€” same-euid interference with active debuggers/tracers (CWE-862) Status: REPRODUCED on DragonFly 6.5-DEVELOPMENT #0, two runs.

What the bug is

procfs_control() deliberately skips the credential gate for DETACH:

if (op != PROCFS_CTL_DETACH) {     /* procfs_ctl.c:122 */
    ... CHECKIO / p_trespass / securelevel checks ...
}

The stated intent ("allow processes to disengage debugging on a process onto which they have previously attached, but no longer have permission to debug") only justifies the exemption for the tracer itself β€” but the code grants it to anyone who can open the ctl file (any same-euid process; mode 0200, owner = target euid). A bystander that never attached anything can write "detach" to /proc/<victim>/ctl and the kernel will, with no authorization:

  • clear P_TRACED (procfs_ctl.c:203),
  • delete the victim's pending SIGTRAP (:206-208),
  • reparent the victim back to p_oppid (:211-219),
  • clear P_WAITED and unstop the victim (:222, :280-281),

i.e. silently terminate an active debugging/tracing session run by another (same-euid, or root) tracer β€” the tracer's subsequent ctl ops fail EBUSY and a root debugger (or any ptrace-based monitoring/sandbox tool holding the session via procfs semantics) loses the tracee mid-flight.

Secondary defect on the same path: wakeup((caddr_t) curp) at :223 wakes a address derived from the detacher, while a tracer blocked in CTL_WAIT tsleeps on (caddr_t) p (:258) β€” a detach issued by anyone other than the WAITer's own process never wakes the blocked tracer (it hangs until a signal breaks the PCATCH sleep).

Reproduced evidence (run.log)

[T 1101] ctl attach V(1100)
[D 1102] bystander writes detach to /proc/1100/ctl
[D] DETACH ACCEPTED with no authorization
[T] ctl step V #1 after interference: failed: Device busy
[T] ctl run V: failed: Device busy

Bystander D (uid 1001, never attached anything, not the tracer, not the parent) successfully detached the victim from tracer T's live session; T's next ctl ops fail EBUSY. Reproduced twice (run.2.log after clean-snapshot reset).

Files

  • df3226.c β€” PoC (all processes unprivileged uid 1001)
  • run.log, run.2.log β€” full output, two runs
  • fix.diff β€” verified fix (exempt DETACH only for the current tracer p->p_pptr == curp; wake p not curp)
  • verdict.json, manifest.json

Build & run

cc -O -o df3226 df3226.c
./df3226
VERDICT.md
↓ download raw

DF-3226 VERDICT β€” REPRODUCED (unauthorized detach of a live tracee by a same-uid bystander)

Bottom line

procfs_control() skips ALL authorization for PROCFS_CTL_DETACH (sys/vfs/procfs/procfs_ctl.c:122). Any process that can open /proc/<victim>/ctl (mode 0200 β†’ any same-euid process, or anyone holding a pre-opened fd across the victim's cred changes) β€” not just the tracer β€” can detach a victim from its active tracer: P_TRACED cleared, pending SIGTRAP deleted, victim reparented to p_oppid and unstopped. The tracer (possibly root β€” a debugger or a ptrace-style monitoring/sandbox session on a uid-U victim) silently loses the tracee; its subsequent ctl operations fail EBUSY. Demonstrated twice as uid 1001 (run.log, run.2.log).

Additionally, wakeup((caddr_t) curp) at procfs_ctl.c:223 uses the detacher's proc pointer as the wake address while a tracer blocked in CTL_WAIT sleeps on (caddr_t) p (procfs_ctl.c:258) β€” a third-party detach never wakes the blocked tracer.

Observed (run.log)

[T 1101] ctl attach V(1100)
[D 1102] bystander writes detach to /proc/1100/ctl
[D] DETACH ACCEPTED with no authorization
[T] ctl step V #1 after interference: failed: Device busy
[T] ctl run V: failed: Device busy

D was never the tracer, never the parent, never attached anything: the write was accepted and destroyed T's session. Same output on the second run after a clean-snapshot reset.

Impact

Integrity/availability of debugging and ptrace/procfs-based supervision sessions: a malicious same-uid process can unstick a victim from analysis or containment at will (break root-run debuggers, evade procfs-style tracing tools). No memory corruption; no uid0 route. Low severity (same-euid scope via the open gate), certain confidence.

Fix

fix.diff (v2, the validated version): DETACH of a P_TRACED process is restricted to the current tracer (p->p_pptr == curp β€” the tracer may still disengage after privilege loss, the original intent; untraced targets keep the harmless no-op path), mirroring PT_DETACH (sys_process.c:250-262), plus the wake-address fix (wakeup((caddr_t) p)).

Validation note: a first fix attempt (v1, simply applying CHECKIO/p_trespass to non-tracer DETACH β€” kept as run.fixed.log) was insufficient: CHECKIO legitimately passes for same-uid bystanders, so the bystander detach was still accepted. v2 (tracer-only) is what was re-validated on a second patched kernel: bystander β†’ EPERM, tracer's session intact (run.fixed2.log).

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: bystander detach accepted, tracer session destroyed (EBUSY afterwards). Patched v2: bystander detach refused EPERM, tracer's step succeeds (run.fixed2.log).

findings/poc/DF-3226/run.fixed2.log (v2 pass); run.fixed.log kept as the documented v1-insufficient iteration
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Tue Sep 8 20:08:46 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log/run.2.log: '[D 1102] bystander writes detach to /proc/1100/ctl', '[D] DETACH ACCEPTED with no authorization', '[T] ctl step V #1 after interference: failed: Device busy', '[T] ctl run V: failed: Device busy'. run.fixed2.log (patched): '[D] detach failed: Operation not permitted', '[T] ctl step V #1 ... ok'.

PoC changes

PoC written fresh (tracer T, victim V, bystander D, all uid 1001).

Verified recommended fix

Restrict DETACH of a P_TRACED process to the current tracer (p->p_pptr == curp), mirroring PT_DETACH; wakeup((caddr_t) p) instead of curp for CTL_WAIT.

Verdict

REPRODUCED on DragonFly 6.5-DEVELOPMENT #0, two boots. procfs_control() exempts DETACH from ALL authorization (procfs_ctl.c:122), so any process that can open /proc//ctl (any same-euid bystander; mode 0200 owner=target euid) can, without ever being the tracer, clear P_TRACED, delete the pending SIGTRAP, reparent the victim to p_oppid, clear P_WAITED and unstop it β€” silently terminating an active tracer's session (demonstrated: bystander D's 'detach' accepted; tracer T's subsequent ctl step/run fail EBUSY). Secondary: wakeup((caddr_t) curp) at :223 wakes the detacher's pointer while a blocked CTL_WAIT tracer sleeps on (caddr_t) p (:258), so third-party detaches never wake it. Impact is same-euid interference with debugging/monitoring (sandbox-style) sessions; same-uid kill(2) already allows coarser damage, hence Low/impact 'none' among the standard classes. Fix validated: with DETACH restricted to the current tracer (mirroring PT_DETACH), the bystander gets EPERM and the tracer's session survives.