# 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.

---

## 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)`:

```c
/* 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.

## Recommended fix

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`.
