Fix crashes reading /proc/<pid> of a task that is exiting - #2772
Open
emkey1 wants to merge 1 commit into
Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Repeatedly
opendir()ing/proc/<pid>/fdof 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.
_ESRCHreturned from aboolfunctionproc_pid_fd_readdirreturns "did I produce another entry", but the task-lookup failure path returned_ESRCH. That is nonzero, so it reads astrue, andproc_readdirgoes on to use a*next_entrythis path never wrote.2.
do_exitreleases mm/files/fs outsidepids_lockA
/proc/<pid>reader holdspids_lockacross its whole access, butdo_exitfrees and clearsmm,filesandfsbefore taking it. The reader can loadtask->files, havedo_exitfree and clear it, then dereference it.kernel/exit.calready handles exactly this forsighand— "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_readdirand the fd/exe/cwd readlinks now also check for NULL, as theauxvandcmdlinehandlers 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_releaseandfs_info_releasetake fs, mount and mem locks in turn, so holdingpids_lockacross them inverts the order against paths that take those first.This version avoids that:
pids_lockis 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 underpids_locktherefore 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_getbounds-checks the wrong tableIt 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, orfdtable_releasewalking a table afterdo_exitclearedcurrent->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.Also ran a shell smoke test over an Alpine rootfs (directory walks,
/procreads of live and exiting children, fork/exec churn, redirects, pipes) 20x, plus a 200-way concurrentls -la+/proc/self/maps+readlink /proc/self/exeworkload. 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 testfails identically with and without this patch here (float80ande2eboth fail on unmodified master on Apple Silicon), so it was not a useful signal either way.🤖 Generated with Claude Code