Skip to content

Fix crashes reading /proc/<pid> of a task that is exiting - #2772

Open
emkey1 wants to merge 1 commit into
ish-app:masterfrom
emkey1:fix_proc_pid_fd_readdir
Open

Fix crashes reading /proc/<pid> of a task that is exiting#2772
emkey1 wants to merge 1 commit into
ish-app:masterfrom
emkey1:fix_proc_pid_fd_readdir

Conversation

@emkey1

@emkey1 emkey1 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Repeatedly opendir()ing /proc/<pid>/fd of a child that exits during the walk kills ish with SIGSEGV. 20 runs out of 20 on master, 0 out of 20 with this change. Three separate things go wrong on that path.

1. _ESRCH returned from a bool function

proc_pid_fd_readdir returns "did I produce another entry", but the task-lookup failure path returned _ESRCH. That is nonzero, so it reads as true, and proc_readdir goes on to use a *next_entry this path never wrote.

2. do_exit releases mm/files/fs outside pids_lock

A /proc/<pid> reader holds pids_lock across its whole access, but do_exit frees and clears mm, files and fs before taking it. The reader can load task->files, have do_exit free and clear it, then dereference it.

kernel/exit.c already handles exactly this for sighand"must be released below so it can be protected by pids_lock since it can be accessed by other threads" — so this does the same for the other three. proc_pid_fd_readdir and the fd/exe/cwd readlinks now also check for NULL, as the auxv and cmdline handlers already did.

Relationship to #2300. @ShoichiroKitano reported this same problem 959 days ago (found via tmux, same root cause) and fixed it by moving lock(&pids_lock) up so it is held across the three release calls. That drew the objection that it breaks lock ordering, which looks fair to me: mm_release, fdtable_release and fs_info_release take fs, mount and mem locks in turn, so holding pids_lock across them inverts the order against paths that take those first.

This version avoids that: pids_lock is held only long enough to detach the three pointers, no second lock is taken inside that window, and the releasing happens after unlocking. A reader under pids_lock therefore sees either a pointer that is still valid or NULL, and the lock ordering is unchanged. Credit to @ShoichiroKitano for the original diagnosis — if you would rather take their PR with this adjustment, that works just as well.

3. fdtable_get bounds-checks the wrong table

if (f < 0 || (unsigned) f >= current->files->size)
    return NULL;
return table->files[f];

It validates against the calling task's table while indexing the one it was passed. Those differ whenever the table is not the caller's own — reading another task's /proc/<pid>/fd, or fdtable_release walking a table after do_exit cleared current->files. So it either indexed out of bounds or, once the pointer was cleared, faulted on NULL. Fixing 2 without this one just moves the crash here.

Testing

Repro: 400 rounds of forking a child that exits after 300us while the parent walks /proc/<pid>/fd.

result
Linux (x86 Ubuntu) completes clean
master SIGSEGV, 20/20 runs
this branch completes clean, 20/20 runs

Also ran a shell smoke test over an Alpine rootfs (directory walks, /proc reads of live and exiting children, fork/exec churn, redirects, pipes) 20x, plus a 200-way concurrent ls -la + /proc/self/maps + readlink /proc/self/exe workload. No regressions. That shell smoke also reproduces the crash on master, but only under machine load, so the dedicated repro above is the reliable signal.

meson test fails identically with and without this patch here (float80 and e2e both fail on unmodified master on Apple Silicon), so it was not a useful signal either way.

🤖 Generated with Claude Code

Repeatedly opendir()ing /proc/<pid>/fd of a child that exits during the
walk kills ish with SIGSEGV: 20 runs out of 20 on master, 0 out of 20
with this change. Three things go wrong on that path:

- proc_pid_fd_readdir returns bool -- "did I produce another entry" --
  but the task-lookup failure path returned _ESRCH. That is nonzero, so
  it reads as true and proc_readdir goes on to use a *next_entry this
  path never wrote.

- do_exit drops mm, files and fs before taking pids_lock, while a
  /proc/<pid> reader holds pids_lock across its whole access. So the
  reader can load task->files, have do_exit free and clear it, and then
  dereference it. The file already handles this for sighand ("must be
  released below so it can be protected by pids_lock"); do the same for
  the other three by detaching them under the lock and releasing after.
  proc_pid_fd_readdir, and the fd/exe/cwd readlinks, now check for NULL
  as the auxv and cmdline handlers already did.

  ish-app#2300 reported this same problem (found via tmux) and fixed it by
  holding pids_lock across the three release calls. That drew the
  objection that it breaks lock ordering, which seems fair: those
  release paths take fs, mount and mem locks in turn. Detaching the
  pointers under the lock and releasing outside it takes no second lock
  while pids_lock is held, so the ordering is unchanged.

- fdtable_get bounds-checked f against current->files->size while
  indexing the table it was passed. Those differ whenever the table is
  not the calling task's own -- reading another task's /proc/<pid>/fd, or
  fdtable_release walking a table after do_exit cleared current->files --
  so it either indexed out of bounds or faulted on NULL.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants