On Wed, Oct 28, 2020 at 11:53 PM Kees Cook <keescook@xxxxxxxxxxxx> wrote: > On Mon, Oct 26, 2020 at 10:51:02AM +0100, Jann Horn wrote: > > The problem is the scenario where a process is interrupted while it's > > waiting for the supervisor to reply. > > > > Consider the following scenario (with supervisor "S" and target "T"; S > > wants to wait for events on two file descriptors seccomp_fd and > > other_fd): > > > > S: starts poll() to wait for events on seccomp_fd and other_fd > > T: performs a syscall that's filtered with RET_USER_NOTIF > > S: poll() returns and signals readiness of seccomp_fd > > T: receives signal SIGUSR1 > > T: syscall aborts, enters signal handler > > T: signal handler blocks on unfiltered syscall (e.g. write()) > > S: starts SECCOMP_IOCTL_NOTIF_RECV > > S: blocks because no syscalls are pending > > Oooh, yes, ew. Thanks for the illustration. > > Thinking about this from userspace's least-surprise view, I would expect > the "recv" to stay "queued", in the sense we'd see this: > > S: starts poll() to wait for events on seccomp_fd and other_fd > T: performs a syscall that's filtered with RET_USER_NOTIF > S: poll() returns and signals readiness of seccomp_fd > T: receives signal SIGUSR1 > T: syscall aborts, enters signal handler > T: signal handler blocks on unfiltered syscall (e.g. write()) > S: starts SECCOMP_IOCTL_NOTIF_RECV > S: gets (stale) seccomp_notif from seccomp_fd > S: sends seccomp_notif_resp, receives ENOENT (or some better errno?) > > This is not at all how things are designed internally right now, but > that behavior would work, yes? It would be really ugly, but it could theoretically be made to work, to some degree. The first bit of trouble is that currently the notification lives on the stack of the target process. If we want to be able to show userspace the stale notification, we'd have to store it elsewhere. And since we really don't want to start randomly throwing -ENOMEM in any of this stuff, we'd basically have to store it in pre-allocated memory inside the filter. The second bit of trouble is that if the supervisor is so oblivious that it doesn't realize that syscalls can be interrupted, it'll run into other problems. Let's say the target process does something like this: int func(void) { char pathbuf[4096]; sprintf(pathbuf, "/tmp/blah.%d", some_number); mount("foo", pathbuf, ...); } and mount() is handled with a notification. If the supervisor just reads the path string and immediately passes it into the real mount() syscall, something like this can happen: target: starts mount() target: receives signal, aborts mount() target: runs signal handler, returns from signal handler target: returns out of func() supervisor: receives notification supervisor: reads path from remote buffer supervisor: calls mount() but because the stack allocation has already been freed by the time the supervisor reads it, the supervisor just reads random garbage, and beautiful fireworks ensue. So the supervisor *fundamentally* has to be written to expect that at *any* time, the target can abandon a syscall. And every read of remote memory has to be separated from uses of that remote memory by a notification ID recheck. And at that point, I think it's reasonable to expect the supervisor to also be able to handle that a syscall can be aborted before the notification is delivered.