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

TIOCSTI unrestricted terminal input injection with no killswitch

Field Value
ID DF-0005
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N
CWE CWE-840 Business Logic Errors; CWE-20 Improper Input Validation
File sys/kern/tty.c
Lines 1158-1174
Area kern
Confidence certain
Discovered 2026-06-29
Reported pending

Summary

TIOCSTI is permitted for any local unprivileged user who holds their controlling terminal open for read (the normal case β€” every interactive session has /dev/tty). It injects an arbitrary byte into the tty input queue via the line discipline's l_rint, enabling keystroke injection into any other process sharing that terminal (e.g. a setuid program, a privileged daemon, or a sandboxed child). Unlike Linux (dev.tty.legacy_tiocsti) and recent OpenBSD, DragonFlyBSD provides no sysctl/capability to disable it, so the injection primitive is always available.

Root cause

In ttioctl, the TIOCSTI case (sys/kern/tty.c:1158-1174) gates only the fallback cases with caps_priv_check_td(SYSCAP_RESTRICTEDROOT):

case TIOCSTI:            /* simulate terminal input */
    if ((flag & FREAD) == 0 &&
        caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT))   /* tty.c:1159-1161 */
    {
        ...
        return (EPERM);
    }
    if (!isctty(p, tp) &&
        caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT))   /* tty.c:1166-1168 */
    {
        ...
        return (EACCES);
    }
    (*linesw[tp->t_line].l_rint)(*(u_char *)data, tp);   /* tty.c:1173 */

A normal user satisfies both bypass conditions β€” they open /dev/tty or their pts slave O_RDWR (so flag & FREAD is set) and isctty() is true (sys/sys/tty.h) β€” so neither privilege check fires. Execution then reaches l_rint, pushing the attacker-supplied byte into the raw queue exactly as if it had been typed. There is no global enable/disable knob anywhere in the tree (no sysctl gates TIOCSTI; the only gate is the FREAD/isctty test, which a tty owner always passes).

Threat model & preconditions

  • Attacker position: any local unprivileged user who can open their controlling terminal (trivial β€” every interactive session has /dev/tty).
  • Privileges gained or impact: terminal input injection across a privilege/trust boundary. Bytes are read by whichever process next reads the tty, including setuid-root utilities (e.g. su/sudo/passwd-style prompts), privileged daemons attached to the pty, or sandboxed applications whose output the attacker can capture. Realistic outcomes include confused-deputy command injection into a privileged reader and sandbox escape via the controlling pty.
  • Required config or capabilities: none beyond a tty; default kernel.
  • Reachability: /dev/tty (tty_tty.c cttyioctl β†’ VOP_IOCTL β†’ ttioctl), and any pts/ptmx slave the attacker owns.

Proof of concept

PoC source: findings/poc/DF-0005/tiocsti.c

Build & run

cc -o tiocsti findings/poc/DF-0005/tiocsti.c
./tiocsti "echo INJECTED_BY_TIOCSTI >/tmp/pwned"     # as a non-root user, in a tty

Expected output

[+] injected 39 bytes into the tty input queue
[+] they will be read by the next reader of this tty

When the consuming shell next reads, the injected command is processed as if typed, creating /tmp/pwned. No EPERM/EACCES is returned. A privilege-gain variant races the injection against a setuid program that reads a password/command from the tty (program-specific).

Impact

A persistent, unmitigated input-injection primitive for any local user with a controlling terminal. The actual privilege gain depends on a victim program reading the injected bytes, which is why this is rated Low rather than higher. The absence of any disable knob means hardened/multi-user systems cannot neutralize it, unlike peer OSes.

Add a knob to disable TIOCSTI system-wide (default-on for compatibility, with the documented secure option of default-off), mirroring dev.tty.legacy_tiocsti.

--- a/sys/kern/tty.c
+++ b/sys/kern/tty.c
@@ -105,6 +105,15 @@ MALLOC_DEFINE(M_TTYS, "ttys", "tty data structures");
+#ifdef TIOCSTI_DISABLE_DEFAULT
+static int tty_tiocsti_enable = 0;
+#else
+static int tty_tiocsti_enable = 1;
+#endif
+SYSCTL_INT(_kern, OID_AUTO, tty_tiocsti, CTLFLAG_RW, &tty_tiocsti_enable, 0,
+    "Enable TIOCSTI terminal input injection (0=deny)");
+
@@ -1158,6 +1167,11 @@ ttioctl(struct tty *tp, u_long cmd, void *data, int flag)
    case TIOCSTI:           /* simulate terminal input */
+       if (!tty_tiocsti_enable) {
+           lwkt_reltoken(&p->p_token);
+           lwkt_reltoken(&tp->t_token);
+           return (EPERM);
+       }
        if ((flag & FREAD) == 0 &&
            caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT))
        {

This gives operators a single-knob way to neutralize the injection primitive on multi-user or hardened systems while preserving historical behavior by default. A stronger follow-up is to gate TIOCSTI behind SYSCAP_RESTRICTEDROOT when kern.tty_tiocsti == 0.

References

  • sys/kern/tty.c:1158-1174 β€” TIOCSTI handling (privilege bypass).
  • Linux dev.tty.legacy_tiocsti (commit / Documentation).
  • OpenBSD TIOCSTI restriction.
  • CWE-840 Business Logic Errors; CWE-20 Improper Input Validation.

Timeline

  • 2026-06-29 Discovered during automated file-by-file audit of sys/kern/tty.c.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0005 Β· 16 files
FileTypeDescriptionSize
tiocsti.c trigger-source self-contained TIOCSTI PoC: pty pair + unprivileged child claims ctty + injects via /dev/tty (no-EPERM proof, readback, downstream-shell execution) 6.3 KB view raw
build.sh build-script cc -Wall -o tiocsti tiocsti.c 210 B view raw
run.sh run-script ./tiocsti (no external tty required) 293 B view raw
build.log build-log final successful build, full output 66 B view raw
run.log run-log decisive run #1, full output (uid=1001, no EPERM, readback, downstream exec) 406 B view raw
run.2.log run-log stress run #2 (identical result) 406 B view raw
run.3.log run-log stress run #3 (identical result) 406 B view raw
baseline_unpatched.log baseline-run-log Phase 8 'before' half: PoC on #0 unpatched kernel reproducing the bug (sysctl oid does not exist) 542 B view raw
env.txt environment uname, cc version, unprivileged uid, negative sysctl killswitch search, kernel source refs 1.5 KB view raw
VERDICT.md verdict full narrative: mechanism with path:line, evidence, fix rationale, Phase 8 fix validation 8.8 KB ↓ raw
fix.diff suggested-fix git-apply-able: add kern.tty_tiocsti sysctl killswitch to sys/kern/tty.c (validated on #1) 1.1 KB view raw
fix_build.log fix-build-log full nativekernel build output for the single-fix #1 kernel (rc=0) 5.6 MB ↓ download
fix_run.log fix-run-log Phase 8 'after' half: PoC on #1 kernel across 5 scenarios (default=1 works, =0 EPERM x3, toggle-back works) 2.0 KB view raw
README.md readme human-facing build/run/expected summary 4.5 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human-facing build/run/expected summary
↓ download raw

DF-0005 β€” PoC: TIOCSTI unrestricted terminal input injection (no killswitch)

Status: REPRODUCED on DragonFlyBSD master DEV (v6.5.0.1712.g89e6a-DEVELOPMENT). Unprivileged uid=1001 can inject arbitrary bytes into a controlling terminal via TIOCSTI; the injected bytes are consumed and executed by the next reader of the tty. No EPERM/EACCES is returned and no sysctl/capability killswitch exists to disable it.

The issue

In ttioctl (sys/kern/tty.c:1158-1173) the two caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT) guards (tty.c:1160,1167) are each gated on a condition a legitimate controlling-tty owner does not meet:

Guard Gating condition Value for ctty owner (O_RDWR open of /dev/tty) Result
1st (tty.c:1159-1163) (flag & FREAD) == 0 FALSE β€” FREAD=0x0001 is set (fcntl.h:68) short-circuits β†’ no EPERM
2nd (tty.c:1165-1169) !isctty(p, tp) FALSE β€” isctty is true for the ctty (tty.h:216) short-circuits β†’ no EACCES

Control falls through to the inject sink:

(*linesw[tp->t_line].l_rint)(*(u_char *)data, tp);   /* sys/kern/tty.c:1172 */

…which pushes the attacker byte into the tty input queue exactly as if typed. A grep for tty_tiocsti / legacy_tiocsti across sys/ returns nothing, and on the running guest sysctl kern.tty_tiocsti, kern.legacy_tiocsti, and dev.tty.legacy_tiocsti all return "unknown oid" β€” there is no killswitch.

Build

On the DragonFlyBSD guest, as any user:

cc -Wall -o tiocsti tiocsti.c
# or simply:
./build.sh

Run

As an unprivileged user (no external interactive terminal required β€” the PoC allocates its own pty pair, so it runs cleanly under non-interactive ssh):

./tiocsti
# or inject a custom downstream command:
./tiocsti "echo custom_payload"

Expected output (bug present)

[*] uid=1001 euid=1001  pty slave=/dev/pts/0
[+] TIOCSTI ioctl succeeded (no EPERM/EACCES) -- guards at tty.c:1160,1167 bypassed for ctty owner
[+] readback from /dev/tty (25 bytes): echo TIOCSTI_READBACK_OK
echo TIOCSTI_EXEC_BY_DOWNSTREAM_SHELL
exit
$ echo TIOCSTI_EXEC_BY_DOWNSTREAM_SHELL
TIOCSTI_EXEC_BY_DOWNSTREAM_SHELL          <-- injected command, EXECUTED by the downstream shell
$ exit
[*] downstream sh exit status: 0

This proves all three properties:

  1. No EPERM/EACCES for unprivileged uid=1001 β†’ both privilege guards bypassed for the ctty owner (the core claim).
  2. Bytes reach the l_rint sink (tty.c:1172) β†’ the 25-byte injected line is read straight back out of /dev/tty.
  3. Confused-deputy execution β†’ a different downstream reader of the tty (a shell exec'd on the pty slave) reads the second injected line out of the input queue and executes it. This is the trust-boundary crossing that makes TIOCSTI dangerous (e.g. injecting into a setuid utility reading a password/command from the tty).

On a kernel patched with fix.diff, sysctl kern.tty_tiocsti=0 makes the ioctl return EPERM and the PoC prints TIOCSTI DENIED: Operation not permitted. Fix validated on a built-and-booted single-fix #1 kernel (see fix_build.log, fix_run.log, and the "Fix validation" section of VERDICT.md): default =1 preserves historical behavior; =0 blocks the injection deterministically across 3 consecutive runs; toggle back to =1 restores it (round-trip OK).

The run is deterministic (3/3 identical β€” see run.log, run.2.log, run.3.log) and non-destructive (no panic; guest stays up). Concrete privilege escalation would require a setuid/privileged tty-reader victim, which is program-specific and out of scope for this Low finding β€” hence the finding's Low severity is confirmed accurate.

Files

  • tiocsti.c β€” self-contained trigger source.
  • build.sh / run.sh β€” exact build/run commands.
  • build.log, run.log, run.2.log, run.3.log β€” full untrimmed outputs.
  • baseline_unpatched.log β€” Phase 8 "before": PoC on #0 unpatched kernel.
  • fix_build.log β€” full nativekernel output for the single-fix #1 kernel.
  • fix_run.log β€” Phase 8 "after": PoC on #1 across 5 scenarios.
  • env.txt β€” guest uname, compiler, unprivileged uid, killswitch search.
  • VERDICT.md β€” full mechanism walkthrough with path:line citations + fix validation.
  • fix.diff β€” git-apply-able kern.tty_tiocsti killswitch for sys/kern/tty.c.
  • manifest.json β€” machine-readable artifact catalog.
VERDICT.md verdict full narrative: mechanism with path:line, evidence, fix rationale, Phase 8 fix validation
↓ download raw

DF-0005 β€” VERDICT

REPRODUCED β€” TIOCSTI terminal input injection is unconditionally available to any unprivileged local user with their controlling tty open for read, with no sysctl/capability killswitch on DragonFlyBSD master DEV. Impact: leak:0 (info) class β€” the primitive itself is input-injection / confused-deputy execution, not memory corruption; rated Low because concrete privilege gain depends on a setuid/privileged tty-reader victim (program-specific). The finding's claim, severity, and cited line numbers are all confirmed accurate.

FIX VALIDATED (fixed). A single-fix kernel built from fix.diff (adds a kern.tty_tiocsti sysctl killswitch, default-on for compatibility) closes the primitive: with kern.tty_tiocsti=0 TIOCSTI returns EPERM and no byte reaches the l_rint sink; with the default =1 historical behavior is preserved and the sysctl round-trips cleanly. See "Fix validation" below.


Mechanism (trigger β†’ primitive β†’ effect)

The unprivileged caller opens its controlling terminal (/dev/tty, or a pts slave it owns) O_RDWR and issues ioctl(fd, TIOCSTI, &byte). In sys/kern/tty.c the TIOCSTI case (tty.c:1158-1173) gates only the fallback paths with caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT):

/* tty.c:1158 */  case TIOCSTI:                       /* simulate terminal input */
/* tty.c:1159 */      if ((flag & FREAD) == 0 &&
/* tty.c:1160 */          caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT))
/* tty.c:1161-1163 */  { ...; return (EPERM); }
/* tty.c:1165 */      if (!isctty(p, tp) &&
/* tty.c:1166 */          caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT))
/* tty.c:1167-1169 */  { ...; return (EACCES); }
/* tty.c:1172 */      (*linesw[tp->t_line].l_rint)(*(u_char *)data, tp);   /* SINK */

A legitimate ctty owner defeats both guards:

  1. First guard bypassed β€” flag is the f_flag of the open file (sys/sys/fcntl.h:68 #define FREAD 0x0001); an O_RDWR open has FREAD set, so (flag & FREAD) == 0 is FALSE, the && short-circuits, and the privilege check is never evaluated β†’ no EPERM.
  2. Second guard bypassed β€” isctty(p, tp) is (p->p_session == tp->t_session && (p->p_flags & P_CONTROLT)) (sys/sys/tty.h:216); for the process's own controlling terminal this is TRUE, so !isctty(p, tp) is FALSE β†’ no EACCES.

Control therefore reaches the inject sink tty.c:1172, which calls the line discipline's l_rint to push the attacker byte into the tty input queue exactly as if it had been typed on the keyboard. Whatever process reads the tty next β€” the caller itself, a sibling shell, a setuid utility prompting on the tty, or a privileged daemon β€” receives and acts on the injected bytes. There is no global enable/disable knob anywhere in the tree (a grep for tty_tiocsti / legacy_tiocsti across sys/ returns nothing; on the running guest sysctl kern.tty_tiocsti, kern.legacy_tiocsti, and dev.tty.legacy_tiocsti all return "unknown oid"). Unlike Linux (dev.tty.legacy_tiocsti, default flipping to off) and recent OpenBSD, DragonFlyBSD gives operators no way to neutralize the primitive.

Evidence (decisive run, unprivileged uid=1001 maxx)

./tiocsti three times, identical result:

[*] uid=1001 euid=1001  pty slave=/dev/pts/0
[+] TIOCSTI ioctl succeeded (no EPERM/EACCES) -- guards at tty.c:1160,1167 bypassed for ctty owner
[+] readback from /dev/tty (25 bytes): echo TIOCSTI_READBACK_OK
echo TIOCSTI_EXEC_BY_DOWNSTREAM_SHELL
exit
$ echo TIOCSTI_EXEC_BY_DOWNSTREAM_SHELL
TIOCSTI_EXEC_BY_DOWNSTREAM_SHELL          <-- the injected command, EXECUTED by the downstream shell
$ exit
[*] downstream sh exit status: 0

This proves all three properties end-to-end:

  1. No EPERM/EACCES for unprivileged uid 1001 β†’ both privilege guards bypassed for the ctty owner (the core claim).
  2. Bytes reach the l_rint sink β†’ the 25-byte injected line is read straight back out of /dev/tty.
  3. Confused-deputy execution β†’ a different downstream reader of the tty (a shell exec'd on the slave) reads the second injected line out of the input queue and executes it (TIOCSTI_EXEC_BY_DOWNSTREAM_SHELL is printed as the command's own output). This is the trust-boundary crossing that makes TIOCSTI dangerous (e.g. injecting into a setuid program reading a password/command from the tty).

The run is deterministic (3/3 identical) and non-destructive (no panic, guest stays up). A real privilege-escalation demo would require a setuid tty-reader victim, which is out of scope for this Low finding and program-specific.

PoC changes

The original tiocsti.c only printed "injected N bytes" and required an external interactive terminal (/dev/tty open fails under non-interactive ssh, since there is no controlling tty). I rewrote it to be fully self-contained: it allocates its own /dev/ptmx pty pair, an unprivileged child does setsid() + TIOCSCTTY to claim the slave as its controlling terminal (putting it in the exact position the finding describes), then exercises TIOCSTI on /dev/tty. The PoC now demonstrates the full chain (no-EPERM + readback + downstream-shell execution) with no external dependencies and runs cleanly under non-interactive ssh. Build/run commands unchanged: cc -o tiocsti tiocsti.c then ./tiocsti.

Why this is not a false positive / not already fixed

I traced the exact data flow from attacker input to sink in sys/kern/tty.c on master DEV (commit v6.5.0.1712.g89e6a-DEVELOPMENT, built 2026-06-29). The two caps_priv_check_td(SYSCAP_RESTRICTEDROOT) guards are present but each is short-circuited by the (flag & FREAD) == 0 / !isctty(p, tp) gating the finding describes β€” they protect only the non-owner fallback cases (a process that opened the tty without FREAD, or a tty that is not its controlling terminal). For the legitimate ctty owner (the threat model) neither check fires, and no other gate (sysctl, capability, compile-time option) exists in the tree. The primitive is real and unmitigated on master.

Add a kern.tty_tiocsti sysctl killswitch (default-on for compatibility, with the documented secure option of default-off), mirroring Linux's dev.tty.legacy_tiocsti. The full git-apply-able diff is in fix.diff; it supersedes the finding markdown's proposal (which was sketch-only with incorrect line offsets) β€” this one is line-accurate against the audited tree, drops both tokens only on the deny path, and adds the SYSCTL registration with a description string. A stronger follow-up (also noted) is to additionally gate TIOCSTI behind SYSCAP_RESTRICTEDROOT when kern.tty_tiocsti == 0.


Fix validation (Phase 8 β€” built + booted a single-fix kernel)

The fix.diff was validated end-to-end on a single-fix kernel built from the audited /usr/src tree.

Build. Applied cleanly with patch -p1 (both hunks at the expected offsets β€” sysctl+variable after MALLOC_DEFINE(M_TTYS,...) at tty.c:105, killswitch at the head of the TIOCSTI case at tty.c:1158). Built with make -j6 nativekernel KERNCONF=X86_64_GENERIC β€” rc=0, no warnings/errors on the patched tty.c (-Werror). kernel.stripped overwritten onto the bare /boot/kernel/kernel (the loader's boot name). Full build log in fix_build.log.

Before/after contrast (the decisive evidence):

Kernel kern.tty_tiocsti TIOCSTI result Bytes reach l_rint sink?
#0 unpatched baseline (oid does not exist) succeeds yes (readback + exec)
#1 single-fix, default 1 (compat) succeeds yes (compat preserved)
#1 single-fix, gated 0 EPERM no β€” denied at head of case
#1 single-fix, toggled 1 again succeeds yes (round-trip OK)

The gated state was confirmed deterministic across 3 consecutive PoC runs (all EPERM, all no-injection). The guest stays up after every run (no panic, no wedge) β€” the fix is purely a behavior gate. Full before/after PoC output is in baseline_unpatched.log (the #0 reproduction) and fix_run.log (the #1 five-scenario run: default β†’ gated Γ—3 β†’ toggle-back).

Fix verdict: FIXED. The single-fix kernel closes the primitive exactly as intended β€” operators can now neutralize TIOCSTI system-wide via sysctl kern.tty_tiocsti=0 (returns EPERM, no injection), while default-on preserves historical behavior and the sysctl round-trips cleanly. The finding's "no killswitch" claim is fully resolved.

Patch versions: - unpatched baseline: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (sha256 5dc83dac…) - single-fix kernel: DragonFly 6.5-DEVELOPMENT #1: Thu Jul 2 17:36:19 UTC 2026 (sha256 76151248…)

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix. Baseline #0 (sysctl oid absent): TIOCSTI succeeds, bytes injected, downstream shell executes the injected command (confused-deputy confirmed). Single-fix #1 kernel: with the default kern.tty_tiocsti=1 the historical behavior is preserved (injection works for compat); with kern.tty_tiocsti=0 the ioctl returns EPERM and no byte reaches l_rint (deterministic across 3 consecutive runs); toggling back to =1 restores the injection (round-trip OK). Build rc=0, no warnings on the patched tty.c (-Werror). The fix closes the primitive exactly as designed -- operators can now neutralize TIOCSTI system-wide while default-on preserves compatibility. fix_status=fixed.

baseline #0:   [+] TIOCSTI ioctl succeeded (no EPERM/EACCES) / [+] readback from /dev/tty (25 bytes): echo TIOCSTI_READBACK_OK / TIOCSTI_EXEC_BY_DOWNSTREAM_SHELL  (downstream sh exit 0)
patched #1 kern.tty_tiocsti=1 (default): identical to baseline (compat preserved, exit 0)
patched #1 kern.tty_tiocsti=0 (FIX): [!] TIOCSTI DENIED: Operation not permitted -- primitive is gated (exit 1, x3 runs identical)
patched #1 toggle back to =1: injection works again (exit 0)
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Thu Jul 2 17:36:19 UTC 2026 (sha256 /boot/kernel/kernel = 76151248367fac055f1ce8ee3aea35ad0e55a2063d1d0bf48d03d13d19661f38)

Confirmed kernel references

Detail

Exploit chain

unprivileged TIOCSTI terminal input injection (confused-deputy): any local user who owns a controlling tty can push arbitrary bytes into its input queue via l_rint (sys/kern/tty.c:1172), which are then read and acted on by the next reader of the tty (e.g. a setuid utility prompting on the tty, a privileged daemon, or a sandboxed child). No memory-corruption primitive, hence no slab-grooming/exploit-chain development -- the realistic impact ceiling is program-specific confused-deputy command injection, consistent with the Low rating. Concrete privilege gain would require a specific privileged tty-reader victim (out of scope for this finding).

Evidence (decisive lines)

baseline (#0, unpatched):  [+] TIOCSTI ioctl succeeded (no EPERM/EACCES) -- guards at tty.c:1160,1167 bypassed for ctty owner / [+] readback from /dev/tty (25 bytes): echo TIOCSTI_READBACK_OK / $ echo TIOCSTI_EXEC_BY_DOWNSTREAM_SHELL / TIOCSTI_EXEC_BY_DOWNSTREAM_SHELL / [*] downstream sh exit status: 0
patched #1, kern.tty_tiocsti=1 (default): identical to baseline (compat preserved)
patched #1, kern.tty_tiocsti=0 (gated, 3 runs): [!] TIOCSTI DENIED: Operation not permitted -- primitive is gated / [*] downstream sh exit status: 1 (no readback, no exec)
patched #1, toggle back to =1: injection works again (round-trip OK)

PoC changes

fix.diff unchanged from prior session (validated as-is: both hunks applied at expected offsets tty.c:104 and tty.c:1166 on /usr/src, compiled clean with -Werror, build rc=0). Added to the evidence pack: baseline_unpatched.log (#0 reproduction), fix_build.log (full nativekernel output, 35496 lines, rc=0), fix_run.log (Phase 8 five-scenario run on #1). Updated VERDICT.md with a 'Fix validation' section documenting the before/after contrast, and manifest.json with fix_status/fix_kernel_uname and the new artifact entries. tiocsti.c unchanged (code_hash matches prior session).

Verified recommended fix

Add a kern.tty_tiocsti sysctl killswitch (default-on=1 for compatibility) to sys/kern/tty.c: a static int tty_tiocsti_enable=1 registered via SYSCTL_INT(_kern,OID_AUTO,tty_tiocsti,CTLFLAG_RW,...) after the MALLOC_DEFINE(M_TTYS) block, plus an early 'if (!tty_tiocsti_enable) { lwkt_reltoken(&p->p_token); lwkt_reltoken(&tp->t_token); return (EPERM); }' at the head of the TIOCSTI case (sys/kern/tty.c:1158). Validated on a built-and-booted single-fix #1 kernel: kern.tty_tiocsti=0 deterministically returns EPERM and blocks injection, while =1 preserves historical behavior and round-trips cleanly. The full git-apply-able diff lives in findings/poc/DF-0005/fix.diff; it supersedes the finding markdown's sketch-level proposal (which had incorrect line offsets) with a line-accurate version that drops both tokens only on the deny path.

Verdict

REPRODUCED on the unpatched #0 baseline (sysctl kern.tty_tiocsti does not exist): the self-contained PoC, run as unprivileged uid=1001 maxx, succeeds in injecting bytes via TIOCSTI into the controlling tty -- the 25-byte 'echo TIOCSTI_READBACK_OK' line is read straight back out of /dev/tty, and a second injected line ('echo TIOCSTI_EXEC_BY_DOWNSTREAM_SHELL') is consumed and EXECUTED by a downstream shell exec'd on the pty slave (the confused-deputy proof). No EPERM/EACCES is returned because both caps_priv_check_td(SYSCAP_RESTRICTEDROOT) guards in sys/kern/tty.c:1158-1173 are short-circuited: (flag & FREAD)==0 is FALSE for an O_RDWR open of /dev/tty, and !isctty(p,tp) is FALSE for the controlling terminal. The finding's claim, severity (Low), and cited line numbers are all confirmed accurate. FIX VALIDATED: the fix.diff adds a kern.tty_tiocsti sysctl killswitch (default 1=compat); a single-fix #1 kernel built from it cleanly closes the primitive -- with kern.tty_tiocsti=0 the ioctl returns EPERM and no byte reaches l_rint (deterministic across 3 consecutive runs), while =1 preserves historical behavior and the sysctl round-trips cleanly (toggle back to 1 restores the injection).