# DF-3173 — autofs timeout task torn down on the wrong per-CPU taskqueue

**Finding**: `autofs_trigger_one()` (sys/vfs/autofs/autofs.c) arms each
request's timeout task on the CREATOR's per-cpu taskqueue
(`#define _taskqueue_thread (taskqueue_thread[mycpuid])`, autofs.c:303,
arming at :357-360) but tears it down on the LAST RELEASER's per-cpu
queue (:398-399).  Requests are shared across CPUs (dedup at :324-339)
and the creator can exit early via EINTR (:366-377, e.g. SIGKILL of a
blocked `stat`), so the last releaser routinely runs on a different
CPU than the creator — the teardown then operates on the WRONG
taskqueue, violating subr_taskqueue.c:184-189 ("do NOT SHARE the task
between CPUs").

Two memory-safety outcomes:
- **PANIC (reproduced, deterministic in round 1)**: with the timeout
  callout fired and the task queued on its home queue (ta_pending=1),
  `taskqueue_cancel_timeout()` → `taskqueue_cancel_locked()` runs
  `STAILQ_REMOVE` against the wrong queue's list — the walk in
  sys/sys/queue.h:319-331 runs off the end and dereferences NULL.
- **UAF (code-proven; cross-cpu frees + post-free writes observed)**:
  if the home runner already dequeued the task (ta_pending=0), the
  wrong-queue `taskqueue_drain_timeout()` compares the WRONG queue's
  `tq_running` (subr_taskqueue.c:493) and returns without waiting, so
  `objcache_put(ar)` (autofs.c:400) frees the request while
  `autofs_task()` (:196-211) is still executing on it: AUTOFS_WARN
  prints `ar->ar_path` from freed memory (info leak to console/dmesg)
  and writes ar_error=ETIMEDOUT / ar_wildcards / ar_done /
  ar_in_progress into the freed, reusable object.

## Reproduce (root on the DragonFly guest)

```
sh /root/poc/df3173/build.sh     # kmod + fake_daemon3173 + pending_stat
sh /root/poc/df3173/run.sh       # variant A: EXPECT PANIC in round 1
```
`run.sh` choreographs, per round (vfs.autofs.timeout=1):
  - T0      a real user `stat` (pending_stat: self-pending blocked
            SIGTERM fires at +850ms) creates the request on its cpu;
  - T0+0.1  a long-lived releaser helper on another cpu stats the same
            key (dedup, refcount 2);
  - T0+0.6  the module's SLEEPER task occupies the home queue's runner
            (busy-spinning would starve this kernel's per-cpu softclock
            thread — kern_timeout.c:423 — and the callout would never
            fire);
  - T0+0.85 the pending SIGTERM interrupts the creator's cv wait ->
            EINTR exit, refcount 2->1;
  - T0+1.0  the timeout callout fires; the timeout task is queued
            behind the sleeper on the HOME queue (ta_pending=1);
  - T0+1.2  the fake automountd's AUTOFSDONE wakes the releaser, which
            frees the request on ITS cpu -> cancel/drain on the WRONG
            queue -> foreign-list STAILQ_REMOVE -> NULL deref.

**Expected (stock kernel)**: after `DF3173: R01 ... ta_pending=1`,
```
Fatal trap 12: page fault while in kernel mode
fault virtual address   = 0x0
Stopped at  taskqueue_cancel_locked+0x45: movq (%rcx),%rax
```
(see run.log / panic.txt; reproduced 4x total, including twice
spontaneously during variant-B timing iterations.)

**Expected (kernel + fix.diff)**: `RUN-A-DONE-NO-PANIC`, ~22+ rounds of
`R%02d complete (request freed=yes)`, zero traps (fix_run_dmesg.txt).

Variant B (`runB.sh`) hunts the UAF marker: the module re-gets the
freed request from the freeing cpu's objcache magazine (SAME-BLOCK
prints in runB_markers.log prove the cross-cpu free) and observes
ar_error/done written after the free.

## Fix
`fix.diff`: remember the arming queue in the request
(`ar->ar_taskqueue = taskqueue_thread[mycpuid]` at creation) and use it
for `taskqueue_cancel_timeout`/`taskqueue_drain_timeout` at teardown.
Validated: baseline panic gone, normal autofs behavior preserved.
