# DF-2768 — unix98 pty clone-limit off-by-one: `ptis[MAXPTYS]` OOB read+write

## What it is

`ptyclone()` (sys/kern/tty_pty.c:176) asks for a new unit with

```c
unit = devfs_clone_bitmap_get(&DEVFS_CLONE_BITMAP(pty), MAXPTYS);
```

but `devfs_clone_bitmap_get()` only refuses units **strictly greater**
than its limit (sys/vfs/devfs/devfs_helper.c:224-227):

```c
unit = devfs_clone_bitmap_fff(bitmap);
if (limit > 0 && unit > limit)      /* unit == limit passes! */
        unit = -1;
```

With all units 0..999 allocated, `fff` returns exactly 1000, the check
`1000 > 1000` is false, and unit 1000 is accepted.  `ptis` is allocated
with exactly `MAXPTYS` entries (tty_pty.c:1292:
`kmalloc(sizeof(struct pt_ioctl *) * MAXPTYS, ...)` = 8000 bytes), so
ptyclone then executes, with `unit == 1000`:

* tty_pty.c:186  `if ((pti = ptis[unit]) == NULL)` — **out-of-bounds
  read of uninitialized heap memory, consumed as a `pt_ioctl *` pointer**
  (type-confusion primitive if non-NULL);
* tty_pty.c:190  `ptis[unit] = pti` — **out-of-bounds write of a kernel
  heap pointer 8 bytes past the requested allocation size**.

Today the OOB read yields NULL *by allocator accident*: kmalloc(8000)
rounds to an 8192-byte zone chunk and the M_ZERO path bzeros the full
rounded chunk size (kern_slaballoc.c: zoneindex() rewrites the request
to the chunk size; `bzero(chunk, size)` at the `done:` label), so the
slack at +8000..8191 is zero.  The OOB write therefore lands in dead
slack of ptis' own chunk — no cross-object corruption at MAXPTYS=1000.
If MAXPTYS were a power-of-two multiple (e.g. 1024 → exactly 8192
bytes), `ptis[MAXPTYS]` would instead read/write the **first 8 bytes of
the neighboring heap chunk** — a groomable type-confusion/corruption
primitive.

Observable effect on the stock kernel: a functional **1001st pty** is
created beyond the enforced limit; the OOB slot persists and is reused
across cycles (`ptis` is never freed and `pti_done()` never clears
`ptis[unit]`).

## Reproduce (unprivileged)

```
cc -O2 -Wall -o ptmx_limit_offbyone ptmx_limit_offbyone.c
./ptmx_limit_offbyone          # as ANY user (e.g. nobody)
# expected (vulnerable): "VULNERABLE: /dev/pts/1000 exists ...", rc=2
```

Also in the pack:

* `probe_oob_unit.c` — holds 1000 ptys, shows the 1001st clone creating
  `/dev/pts/1000`, the master open failing (ENODEV via the autoclone
  fallback), `/dev/pts/1000` opening successfully as a slave, and the
  1002nd open being refused.
* `cycle_oob_unit.c` — cycles the OOB unit 5×: each cycle re-creates
  `/dev/pts/1000` and reuses the same pti (`vmstat -m` "ptys" count
  stays at 1002 = ptis array + 1001 ever-allocated ptis), proving the
  OOB slot write at `ptis[1000]` persisted.

Success criterion: `/dev/pts/1000` exists while 1000 ptys are held →
`ptis[1000]` OOB taken.  Fixed kernel: open #1001 refused (ENODEV),
no `/dev/pts/1000`, exactly ≤1000 concurrent ptys.

## Fix

`MAXPTYS - 1` is the highest valid unit.  See fix.diff.
