[Bug 296835] kern: coredump of a process with many EVFILT_VNODE knotes hangs for hours, process unkillable (SIGKILL pending), holds fd table lock (NT_PROCSTAT_KQUEUES note generation)

[email protected] Thu, 16 Jul 2026 09:55:50 +0000
Newsgroups gmane.os.freebsd.bugs
Message-ID <[email protected]/bugzilla/>
https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=296835

            Bug ID: 296835
           Summary: kern: coredump of a process with many EVFILT_VNODE
                    knotes hangs for hours, process unkillable (SIGKILL
                    pending), holds fd table lock (NT_PROCSTAT_KQUEUES
                    note generation)
           Product: Base System
           Version: 15.1-RELEASE
          Hardware: Any
                OS: Any
            Status: New
          Severity: Affects Only Me
          Priority: ---
         Component: kern
          Assignee: [email protected]
          Reporter: [email protected]

## Environment

FreeBSD dal 15.1-RELEASE releng/15.1-n283562-96841ea08dcf GENERIC amd64

## Overview

A process (the "codex" CLI, a Rust program using the notify-rs file watcher
with its kqueue backend) had ~670,000 open file descriptors, each with an
EVFILT_VNODE knote attached (kqueue file watching requires one open fd per
watched vnode; kern.openfiles was 675,453 while the process was alive).

The process received a core-dumping signal at 11:04. More than 35 minutes
later the core file still existed with size 0 and the process was:

- unkillable: SIGINT, SIGTERM and SIGKILL all shown as pending
  (procstat -j: flags "P--" for INT/TERM/KILL) and never delivered;
- shown as T+ in ps (56 of 57 threads suspended in thread_suspend_check
  via the coredump single-threading);
- pinning one CPU: a single thread in state "run" busy inside the kernel;
- blocking fd-table introspection: `procstat -f <pid>` blocks (it emitted
  output only very slowly; the note generation runs the whole knote walk
  under fget_remote_foreach on the target's fd table).

## Kernel stack of the busy thread (procstat -kk)

```
15844 147355 codex  notify-rs kqueue lo
  kmem_back_domain+0x14d kmem_malloc_domainset+0xd2 malloc_large+0x2c
  sbuf_put_bytes+0x16a sbuf_bcat+0xe
  kern_proc_kqueue_report_one+0x185 kern_proc_kqueue_report+0x90
  fget_remote_foreach+0x112 kern_proc_kqueues_out+0x67
  elf64_prepare_notes+0x99c elf64_coredump+0x91 coredump_vnode+0xba2
  sigexit+0x271 postsig+0x23a ast_sig+0x1d7 ast_handler+0x88 ast+0x20
  fast_syscall_common+0x1a2
```

All other 56 threads:

```
  mi_switch+0xbc thread_suspend_check+0x23c ast_suspend+0x31
  ast_handler+0x88 ast+0x20 doreti_ast+0x1c
```

## Observed kernel memory behaviour

`vmstat -m | grep sbuf`, sampled while stuck (memory column in bytes):

```
t+0s    sbuf  5  404443520  260315 ...
t+5s    sbuf  4  202736000  260725 ...
...
t+10m   sbuf  5  425611648  268230 ...
t+10m8s sbuf  4  213578112  268949 ...
t+10m16 sbuf  4  214352256  269613 ...
t+10m24 sbuf  5  430223744  270176 ...
t+10m32 sbuf  4  215875968  270633 ...
t+~20m  sbuf  4  284258688  336461 ...
```

The buffer grows monotonically (~100-300 KB/s); the transient 2x spikes are
the SBUF_AUTOEXTEND doubling (allocate double, copy, free old) done via
malloc_large on a buffer that is already hundreds of MB.

## Analysis

sizeof(struct kinfo_knote) is 1160 bytes because it embeds
knt_vnode_fullpath[PATH_MAX]. With ~670k vnode knotes the
NT_PROCSTAT_KQUEUES note is ~780 MB.

The note is generated twice (imgact_elf.c note_procstat_kqueues): once for
the sizing pass (sb == NULL) and once for the emit pass. Although the
sizing pass hands in an sbuf with sbuf_count_drain, kern_proc_kqueues_out()
(kern_event.c) ignores the drain and buffers the *entire* report in its own
kernel sbuf first:

```c
        s = sbuf_new(&sm, NULL, sb_len, maxlen == -1 ? SBUF_AUTOEXTEND :
            SBUF_FIXEDLEN);
        error = kern_proc_kqueues_out1(curthread, p, s, compat32);
        sbuf_finish(s);
        if (error == 0)
                sbuf_bcat(sb, sbuf_data(s), ...);
```

So the sizing pass alone malloc's ~780 MB of kernel memory just to count
bytes, growing by doubling+copy (malloc_large of hundreds of MB each time),
and the emit pass then does it all again into a second ~780 MB SBUF_FIXEDLEN
buffer.

Per knote, kern_proc_kqueue_report_one() additionally does
kn_enter_flux / KQ_UNLOCK / f_userdump (vn_fullpath for vnode knotes) /
KQ_LOCK / kn_leave_flux, which is what makes the walk proceed at only a few
hundred knotes per second — hours for 670k knotes, times two passes. The
NT_PROCSTAT_FILES note (registered before KQUEUES, also with per-fd
fullpath resolution, kern.coredump_pack_fileinfo=1) has the same shape and
had presumably already consumed part of the elapsed time.

During all of this the process is in sigexit(): further signals, including
SIGKILL, are only marked pending and are never acted on, and the whole knote
walk runs inside fget_remote_foreach() on the process's fd table, so other
consumers of that table (procstat -f) block as well.

## Impact

- A crash of any fd-heavy kqueue-based file watcher (watchman, notify-rs,
  etc. — increasingly common with LLM coding agents watching large source
  trees) turns into an hours-long, unkillable, CPU-pinning core dump that
  transiently allocates ~1.5 GB of kernel malloc memory.
- Unprivileged local DoS vector: any user can open a few hundred thousand
  vnode kevents (subject only to kern.maxfilesperproc, here 1,878,633) and
  raise SIGQUIT; there is no way for the administrator to kill the
  resulting process short of reboot.
- There is no sysctl to disable the kqueue note (kern.coredump_pack_fileinfo
  and kern.coredump_pack_vmmapinfo exist; there is no
  kern.coredump_pack_kqinfo).

## Suggested directions

- Make kern_proc_kqueues_out() honor the caller's sbuf/drain instead of
  buffering the entire note in a private sbuf (the sizing pass would then
  cost no large allocations at all).
- Add a kern.coredump_pack_kqinfo sysctl (parallel to pack_fileinfo), or an
  overall cap on procstat note sizes in core dumps.
- Consider checking for a pending SIGKILL periodically during note
  generation so an administrator can abort a pathological dump.

## How to reproduce

On a machine with enough fds (raise kern.maxfilesperproc if needed), run a
program that opens ~500k files, registers an EVFILT_VNODE kevent for each
on one kqueue, then calls abort(). Observe: 0-byte core file, unkillable
process in T state, one thread spinning in elf64_prepare_notes /
kern_proc_kqueues_out, sbuf malloc type growing to hundreds of MB, and
`procstat -f <pid>` blocking.

## Notes from the live incident

- ps:      `15844 codex resume  T+  elapsed 01:37:28` (parent: interactive
bash)
- core:    /home/giacomo/devel/qobuzconnect2mpd/codex.core, 0 bytes, born 11:04
- signals: procstat -i shows INT, TERM, KILL all pending ("P--")
- fds:     procstat -f had emitted 673,201 lines when we gave up waiting;
           kern.openfiles = 675,453
- memory:  34 GB free RAM, 53 GB free kmem — the allocations themselves
           succeed; the cost is the repeated doubling/copying and the
           per-knote lock/flux/vn_fullpath cycle.

-- 
You are receiving this mail because:
You are the assignee for the bug.