seccomp_unotify(2)
π NAME π
seccomp_unotify β the Seccomp user-space notification mechanism. yes bestie, that's seccomp_unotify(2), straight outta the System Calls Manual, and we're gonna make it make sense π«Ά
π LIBRARY π
Standard C library (libc, -lc). the usual girlies.
β¨ SYNOPSIS β¨
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>
int seccomp(unsigned int operation, unsigned int flags, void *args);
#include <sys/ioctl.h>
int ioctl(int fd, SECCOMP_IOCTL_NOTIF_RECV,
struct seccomp_notif *req);
int ioctl(int fd, SECCOMP_IOCTL_NOTIF_SEND,
struct seccomp_notif_resp *resp);
int ioctl(int fd, SECCOMP_IOCTL_NOTIF_ID_VALID, __u64 *id);
int ioctl(int fd, SECCOMP_IOCTL_NOTIF_ADDFD,
struct seccomp_notif_addfd *addfd);
πΈ DESCRIPTION πΈ
So this page is all about the user-space notification mechanism that the Secure Computing (seccomp) facility gives you. π Besides the SECCOMP_FILTER_FLAG_NEW_LISTENER flag, the SECCOMP_RET_USER_NOTIF action value, and the SECCOMP_GET_NOTIF_SIZES operation (all of which live in seccomp(2)), this whole mechanism also runs on a bunch of related ioctl(2) operations, and those are described down below. slay.
π Overview π
Ok so here's the vibe shift. In conventional seccomp filter usage, the filter itself is the one deciding how a system call gets treated. But the user-space notification mechanism? It lets the seccomp filter delegate the handling of the system call to some other user-space process. π―
Heads up bestie: this mechanism is explicitly not intended as a method of implementing security policy. go read NOTES for that tea.
In everything that follows:
- the thread(s) on which the seccomp filter is installed = the target π―
- the process that gets notified by the user-space notification mechanism = the supervisor π
A suitably privileged supervisor can use the user-space notification mechanism to do actions on behalf of the target. The whole advantage here is that the supervisor will usually be able to pull information about the target and the performed system call that the seccomp filter itself simply cannot get. (A seccomp filter is limited in what info it can obtain and what actions it can perform, because it's running on a virtual machine inside the kernel. it's giving little sandbox, no range π.)
So here's the overview of the steps the target and supervisor go through:
- The target sets up a seccomp filter the usual way, but with two differences:
- The
seccomp(2)flags argument includes the flagSECCOMP_FILTER_FLAG_NEW_LISTENER. Because of that, the return value of a (successful)seccomp(2)call is a new "listening" file descriptor that can be used to receive notifications. Only one "listening" seccomp filter can be installed for a thread. one. single. queen. π - In cases where it's appropriate, the seccomp filter returns the action value
SECCOMP_RET_USER_NOTIF. That return value triggers a notification event. π
- The
- So the supervisor can actually get those notifications using the listening file descriptor, (a duplicate of) that file descriptor has to be passed from the target to the supervisor. One way to do it: pass the file descriptor over a UNIX domain socket connection between the target and the supervisor, using the
SCM_RIGHTSancillary message type described inunix(7). Another way: through the use ofpidfd_getfd(2). pick your fighter β¨ - The supervisor receives notification events on the listening file descriptor. Those events come back as structures of type
seccomp_notif. Because this structure and its size might evolve over kernel versions, the supervisor must first figure out the size of this structure using theseccomp(2)SECCOMP_GET_NOTIF_SIZESoperation, which returns a structure of typeseccomp_notif_sizes. The supervisor allocates a buffer of sizeseccomp_notif_sizes.seccomp_notifbytes to receive notification events. In addition, the supervisor allocates another buffer of sizeseccomp_notif_sizes.seccomp_notif_respbytes for the response (astruct seccomp_notif_respstructure) that it will hand to the kernel (and therefore to the target). sizes first, always π§ - The target then does its workload, which includes system calls that will be controlled by the seccomp filter. Whenever one of these system calls makes the filter return the
SECCOMP_RET_USER_NOTIFaction value, the kernel does not (yet) execute the system call. Instead, execution of the target is temporarily blocked inside the kernel β in a sleep state that is interruptible by signals β and a notification event is generated on the listening file descriptor. π΄π€ - Now the supervisor can repeatedly monitor the listening file descriptor for those
SECCOMP_RET_USER_NOTIF-triggered events. To do that, the supervisor uses theSECCOMP_IOCTL_NOTIF_RECVioctl(2)operation to read information about a notification event; this operation blocks until an event is available. The operation returns aseccomp_notifstructure containing info about the system call the target is attempting. (And as described in NOTES, the file descriptor can also be monitored withselect(2),poll(2), orepoll(7).) π - The
seccomp_notifstructure returned by theSECCOMP_IOCTL_NOTIF_RECVoperation includes the same information (aseccomp_datastructure) that was passed to the seccomp filter. That info lets the supervisor discover the system call number and the arguments for the target's system call. On top of that, the notification event contains the ID of the thread that triggered the notification and a unique cookie value, which is used in subsequentSECCOMP_IOCTL_NOTIF_ID_VALIDandSECCOMP_IOCTL_NOTIF_SENDoperations. πͺ
The information in the notification can be used to discover the values of pointer arguments for the target's system call. (And this is something you literally cannot do from within a seccomp filter. periodt.) One way the supervisor can do this: open the corresponding /proc/tid/mem file (see proc(5)) and read bytes from the location that corresponds to one of the pointer arguments whose value is supplied in the notification event. (The supervisor has to be careful to avoid a race condition that can occur when doing this; see the description of the SECCOMP_IOCTL_NOTIF_ID_VALID ioctl(2) operation below. β οΈ) In addition, the supervisor can access other system information that's visible in user space but which is not accessible from a seccomp filter. main character level access, honestly.
- Having obtained information as per the previous step, the supervisor may then choose to perform an action in response to the target's system call (which, as noted above, is not executed when the seccomp filter returns the
SECCOMP_RET_USER_NOTIFaction value).
One example use case here is about containers. π« The target may be sitting inside a container where it doesn't have sufficient capabilities to mount a filesystem in the container's mount namespace. But the supervisor might be a more privileged process that does have sufficient capabilities to perform the mount operation. rise and grind, supervisor.
- Then the supervisor sends a response to the notification. The kernel uses the information in this response to construct a return value for the target's system call and to provide a value that will be assigned to the
errnovariable of the target.
The response is sent using the SECCOMP_IOCTL_NOTIF_SEND ioctl(2) operation, which transmits a seccomp_notif_resp structure to the kernel. This structure includes a cookie value that the supervisor obtained in the seccomp_notif structure returned by the SECCOMP_IOCTL_NOTIF_RECV operation. That cookie value allows the kernel to associate the response with the target. And yes, this structure must include the cookie value that the supervisor obtained in the seccomp_notif structure returned by the SECCOMP_IOCTL_NOTIF_RECV operation β the cookie is what lets the kernel associate the response with the target. no cookie, no connection πͺβ‘οΈπ―
- Once the notification has been sent, the system call in the target thread unblocks, returning the information that was provided by the supervisor in the notification response. we love a good unblock. π¦
As a little variation on the last two steps: the supervisor can send a response that tells the kernel it should execute the target thread's system call; see the discussion of SECCOMP_USER_NOTIF_FLAG_CONTINUE, below. βοΈ
π IOCTL OPERATIONS π
These are the ioctl(2) operations supported by the seccomp user-space notification file descriptor. For each of these operations, the first (file descriptor) argument of ioctl(2) is the listening file descriptor returned by a call to seccomp(2) with the SECCOMP_FILTER_FLAG_NEW_LISTENER flag. same fd every time, iconic π«
π SECCOMP_IOCTL_NOTIF_RECV π
The SECCOMP_IOCTL_NOTIF_RECV operation (available since Linux 5.0) is how you obtain a user-space notification event. If no such event is currently pending, the operation blocks until an event occurs. The third ioctl(2) argument is a pointer to a structure of the following form, which contains information about the event. This structure must be zeroed out before the call. π§Ό
struct seccomp_notif {
__u64 id; /* Cookie */
__u32 pid; /* TID of target thread */
__u32 flags; /* Currently unused (0) */
struct seccomp_data data; /* See seccomp(2) */
};
The fields in this structure, bestie by bestie:
idβ this is a cookie for the notification. πͺ Each such cookie is guaranteed to be unique for the corresponding seccomp filter.- The cookie can be used with the
SECCOMP_IOCTL_NOTIF_ID_VALIDioctl(2)operation described below. - When returning a notification response to the kernel, the supervisor must include the cookie value in the
seccomp_notif_respstructure that is specified as the argument of theSECCOMP_IOCTL_NOTIF_SENDoperation.
- The cookie can be used with the
pidβ this is the thread ID of the target thread that triggered the notification event. π―flagsβ this is a bit mask of flags providing further information on the event. In the current implementation, this field is always zero. π«₯dataβ this is aseccomp_datastructure containing information about the system call that triggered the notification. This is the same structure that is passed to the seccomp filter. Seeseccomp(2)for details of this structure. π¦
On success, this operation returns 0. On failure, -1 comes back and errno gets set so you know what went wrong π
This one can fail like:
EINVAL(since Linux 5.5) β theseccomp_notifstructure you handed to the call had nonzero fields in it. Rude, but okay.ENOENTβ the target thread got killed by a signal while the notification info was being generated, or the target's blocked system call got interrupted by a signal handler. It's giving "whoops, they're gone" π«
β¨ SECCOMP_IOCTL_NOTIF_ID_VALID β¨
Okay bestie, this operation (available since Linux 5.0) is your vibe check πͺͺ β it tells you whether a notification ID you got from an earlier SECCOMP_IOCTL_NOTIF_RECV operation is still valid. Meaning: does the target still exist, and is its system call still sitting there blocked, waiting for a response?
The third ioctl(2) argument is a pointer to the cookie (id) that SECCOMP_IOCTL_NOTIF_RECV handed you.
Why do you need this?? Because of race conditions, bestie π Specifically the kind that happen when the pid returned by SECCOMP_IOCTL_NOTIF_RECV terminates, and then that same process ID gets reused by a whole different process. Here's the cursed scenario:
- A notification is generated on the listening file descriptor. The returned
seccomp_notifcontains the TID of the target thread (that's thepidfield of the structure). - The target terminates.
- Another thread or process gets created on the system that, by pure coincidence, reuses the TID that got freed when the target died.
- The supervisor
open(2)s the/proc/tid/memfile for the TID it got in step 1, with the intent of (say) poking at the memory location(s) holding the argument(s) of the system call that triggered the notification back in step 1.
And the risk in that whole scenario? The supervisor might end up reading the memory of a completely different process than the target π You dodge that race by following your open(2) call with a SECCOMP_IOCTL_NOTIF_ID_VALID operation, which verifies that the process that generated the notification is still alive. (Little note: if the target terminates after that step, a subsequent read(2) from the file descriptor may return 0, which means end of file. Not a bug, just the universe.)
Head to NOTES for a chat about the other cases where you absolutely must perform SECCOMP_IOCTL_NOTIF_ID_VALID checks π
On success β meaning the notification ID is still valid β this operation returns 0. On failure β meaning the notification ID is no longer valid β -1 comes back and errno is set to ENOENT π«Ά
π SECCOMP_IOCTL_NOTIF_SEND π
This one (available since Linux 5.0) is how you send a notification response back to the kernel. The third ioctl(2) argument is a pointer to a structure that looks exactly like this:
struct seccomp_notif_resp {
__u64 id; /* Cookie value */
__s64 val; /* Success return value */
__s32 error; /* 0 (success) or negative error number */
__u32 flags; /* See below */
};
And here's what each field is doing:
idβ the cookie value you got from theSECCOMP_IOCTL_NOTIF_RECVoperation. This cookie is how the kernel correctly associates your response with the system call that triggered the user-space notification. Match made in heaven πvalβ the value that'll be used for a spoofed success return for the target's system call; details below.errorβ the value that'll be used as the error number (errno) for a spoofed error return for the target's system call; details below.flagsβ a bit mask that includes zero or more of these flags:SECCOMP_USER_NOTIF_FLAG_CONTINUE(since Linux 5.5) β tell the kernel to execute the target's system call.
So there are two kinds of response you can send, and they are not the same vibe:
- A response telling the kernel to execute the target's system call. For this one,
flagsincludesSECCOMP_USER_NOTIF_FLAG_CONTINUE, and both theerrorandvalfields must be zero. This kind is a slay when the supervisor needs to do a deeper analysis of the target's system call than a seccomp filter could ever manage (think: inspecting the values of pointer arguments), and after deciding the system call doesn't need emulation by the supervisor, the supervisor wants it to just run normally in the target. That said β useSECCOMP_USER_NOTIF_FLAG_CONTINUEwith caution β οΈ See NOTES. - A spoofed return value for the target's system call. Here the kernel does not execute the target's system call β instead it makes the system call return a spoofed value, as specified by the fields of the
seccomp_notif_respstructure. The supervisor should set the fields like so:flagsdoes not containSECCOMP_USER_NOTIF_FLAG_CONTINUE.erroris set either to0for a spoofed "success" return, or to a negative error number for a spoofed "failure" return. In the first case, the kernel makes the target's system call return the value in thevalfield. In the second case, the kernel makes the target's system call return-1, anderrnogets assigned the negated error value.valis set to whatever value should be used as the return value for a spoofed "success" return for the target's system call. If theerrorfield contains a nonzero value, the value in this field is ignored, so don't stress about it πΈ
On success, this operation returns 0; on failure, -1 is returned and errno is set to indicate the error. It can fail with:
EINPROGRESSβ a response to this notification has already been sent. Yeah, someone already hit send bestie πEINVALβ an invalid value was specified in theflagsfield.EINVALβ theflagsfield containedSECCOMP_USER_NOTIF_FLAG_CONTINUE, and theerrororvalfield was not zero.ENOENTβ the blocked system call in the target has been interrupted by a signal handler, or the target has terminated.
πΈ SECCOMP_IOCTL_NOTIF_ADDFD πΈ
Now THIS one (available since Linux 5.9) lets the supervisor install a file descriptor right into the target's file descriptor table. Very "let me just put this here for you" energy π«Ά Much like the use of SCM_RIGHTS messages described in unix(7), this operation is semantically equivalent to duplicating a file descriptor out of the supervisor's file descriptor table and into the target's file descriptor table.
What's it for? The SECCOMP_IOCTL_NOTIF_ADDFD operation lets the supervisor emulate a target system call that generates a file descriptor, like socket(2) or openat(2). The supervisor can go do the system call that generates the file descriptor (and the associated open file description), then use this operation to allocate a file descriptor referring to that same open file description in the target. (If you need the full lore on open file descriptions, go read open(2).)
Once this operation has been performed, the supervisor can close its copy of the file descriptor. Clean up after yourself, queen π
In the target, the received file descriptor is subject to the same Linux Security Module (LSM) checks as a file descriptor received in an SCM_RIGHTS ancillary message. And if the file descriptor refers to a socket, it inherits the cgroup version 1 network controller settings (classid and netprioidx) of the target.
The third ioctl(2) argument is a pointer to a structure of this form:
struct seccomp_notif_addfd {
__u64 id; /* Cookie value */
__u32 flags; /* Flags */
__u32 srcfd; /* Local file descriptor number */
__u32 newfd; /* 0 or desired file descriptor
number in target */
__u32 newfd_flags; /* Flags to set on target file
descriptor */
};
And the fields break down like this:
idβ set this to the notification ID (cookie value) you got viaSECCOMP_IOCTL_NOTIF_RECV.flagsβ a bit mask of flags that modify the behavior of the operation, described below:SECCOMP_ADDFD_FLAG_SETFDβ when allocating the file descriptor in the target, use the file descriptor number specified in thenewfdfield.SECCOMP_ADDFD_FLAG_SEND(since Linux 5.14) β do the equivalent ofSECCOMP_IOCTL_NOTIF_ADDFDplusSECCOMP_IOCTL_NOTIF_SENDas one atomic operation β¨ On successful invocation, the target process'serrnowill be0and the return value will be the file descriptor number that was allocated in the target. If allocating the file descriptor in the target fails, the target's system call just stays blocked until a successful response is sent. No pressure.
srcfdβ set this to the number of the file descriptor in the supervisor that's getting duplicated.newfdβ this field decides which file descriptor number gets allocated in the target. If theSECCOMP_ADDFD_FLAG_SETFDflag is set, then this field specifies the file descriptor number that should be allocated. If that file descriptor number is already open in the target, it gets atomically closed and reused. But if the descriptor duplication fails because of an LSM check, or ifsrcfdisn't a valid file descriptor, thennewfdwill not be closed in the target process. If theSECCOMP_ADDFD_FLAG_SETFDflag it not set, then this field must be0, and the kernel allocates the lowest unused file descriptor number in the target.newfd_flagsβ a bit mask specifying flags that should be set on the file descriptor received in the target process. Right now only this one flag is implemented:O_CLOEXECβ set the close-on-exec flag on the received file descriptor.
On success, this ioctl(2) call returns the number of the file descriptor that was allocated in the target π Assuming the emulated system call is one that returns a file descriptor as its function result (like socket(2)), this value can be used as the return value (resp.val) that you supply in the response subsequently sent with the SECCOMP_IOCTL_NOTIF_SEND operation.
On error, -1 is returned and errno is set to indicate the error.
This operation can fail with:
EBADFβ allocating the file descriptor in the target would push the target'sRLIMIT_NOFILElimit over the edge (seegetrlimit(2)).EBUSYβ ngl this one only shows up when you passed theSECCOMP_IOCTL_NOTIF_SENDflag: it means the op literally cannot go through until the other pendingSECCOMP_IOCTL_NOTIF_ADDFDrequests get processed first. queue's full bestie, wait your turn π«ΆEINPROGRESSβ the notification you named in theidfield exists, but it's giving "not ready yet": either nobody has fetched it via aSECCOMP_IOCTL_NOTIF_RECVyet, or it's already been answered with aSECCOMP_IOCTL_NOTIF_SEND. either way, there's nothing for you to do with it right now πEINVALβ you handed over a flag that isn't valid in theflagsornewfd_flagsfield, ornewfdis nonzero while theSECCOMP_ADDFD_FLAG_SETFDflag was not set inflags. basically: you can't ask for a specific fd number without the flag that says you're allowed to. πEMFILEβ the fd number you put innewfdblows past the limit set in/proc/sys/fs/nr_open. too many fds, girl, the system said no more. πENOENTβ the blocked syscall in the target got interrupted by a signal handler, or the target straight up terminated. it's gone, bestie, it's not coming back π
Here's some sample code (error handling omitted, bc we keep it cute and simple) that uses the SECCOMP_ADDFD_FLAG_SETFD operation β in this case, to emulate a call to openat(2):
int fd, removeFd;
fd = openat(req->data.args[0], path, req->data.args[2],
req->data.args[3]);
struct seccomp_notif_addfd addfd;
addfd.id = req->id; /* Cookie from SECCOMP_IOCTL_NOTIF_RECV */
addfd.srcfd = fd;
addfd.newfd = 0;
addfd.flags = 0;
addfd.newfd_flags = O_CLOEXEC;
targetFd = ioctl(notifyFd, SECCOMP_IOCTL_NOTIF_ADDFD, &addfd);
close(fd); /* No longer needed in supervisor */
struct seccomp_notif_resp *resp;
/* Code to allocate 'resp' omitted */
resp->id = req->id;
resp->error = 0; /* "Success" */
resp->val = targetFd;
resp->flags = 0;
ioctl(notifyFd, SECCOMP_IOCTL_NOTIF_SEND, resp);
like, the whole vibe is: grab the cookie from SECCOMP_IOCTL_NOTIF_RECV, do the thing the target asked for, inject the resulting fd back in with SECCOMP_IOCTL_NOTIF_ADDFD, and then tell the target everything went great with SECCOMP_IOCTL_NOTIF_SEND. β¨
π NOTES π
Okay bestie, storytime, because this is where it gets real interesting. One iconic use case for the whole user-space notification situation is letting a container manager β a process that's usually running with more privilege than everything inside the container β mount block devices or create device nodes for the container. π
And the mount use case? That's the perfect lil demo of when the SECCOMP_USER_NOTIF_FLAG_CONTINUE ioctl(2) operation is actually useful. Like, the moment the container manager (the "supervisor" β¨) gets a notification for the mount(2) system call, it can tell apart a request to mount a block filesystem (which a "target" process inside the container could never pull off) and mount that file system itself. But if the container manager clocks that the operation could be done by the process inside the container (say, a mount of a tmpfs(5) filesystem), it can just notify the kernel that the target process's mount(2) system call can carry on. π¦
π select()/poll()/epoll semantics π
So, the file descriptor you get back when seccomp(2) is used with the SECCOMP_FILTER_FLAG_NEW_LISTENER flag? You can monitor it with poll(2), epoll(7), and select(2). And these interfaces will let you know the fd is ready like this:
- When a notification is pending, these interfaces say the file descriptor is readable. After a vibe-check like that, a following
SECCOMP_IOCTL_NOTIF_RECVioctl(2)will not block β it'll either hand you info about a notification, or fail with the errorEINTRif the target got killed by a signal or its system call got interrupted by a signal handler. π - Once the notification has been received (i.e., via the
SECCOMP_IOCTL_NOTIF_RECVioctl(2)operation), these interfaces say the file descriptor is writable, meaning you can send a notification response using theSECCOMP_IOCTL_NOTIF_SENDioctl(2)operation. π - After the last thread using the filter has terminated and been reaped using
waitpid(2)(or something similar), the file descriptor shows an end-of-file condition (readable inselect(2);POLLHUP/EPOLLHUPinpoll(2)/epoll_wait(2)). π
π Design goals; use of SECCOMP_USER_NOTIF_FLAG_CONTINUE π
The whole intent of this user-space notification feature is to let system calls be performed on behalf of the target. The target's system call should either be handled by the supervisor, or be allowed to continue normally in the kernel (where all the standard security policies get applied). No cap. π
But listen β note well: this mechanism must not be used to make security policy decisions about the system call. That would be inherently race-prone, and here's why.
The SECCOMP_USER_NOTIF_FLAG_CONTINUE flag must be used with caution. If the supervisor sets it, the target's system call will continue. However, there's a time-of-check, time-of-use race right there, since an attacker could exploit the window of time where the target is blocked waiting on the "continue" response to do stuff like rewriting the system call arguments. π
And also, note: a user-space notifier can be bypassed if the existing filters allow the use of seccomp(2) or prctl(2) to install a filter that returns an action value with a higher precedence than SECCOMP_RET_USER_NOTIF (see seccomp(2)).
So it should be absolutely clear that the seccomp user-space notification mechanism can not be used to implement a security policy! π¨
It should only ever be used in scenarios where a more privileged process supervises the system calls of a lesser privileged target, to get around kernel-enforced security restrictions when the supervisor deems this safe. In other words: in order to continue a system call, the supervisor should be sure that another security mechanism, or the kernel itself, will sufficiently block the system call if its arguments get rewritten to something unsafe. Periodt. π
π§ Caveats regarding the use of /proc/tid/mem π§
So earlier we mentioned you need the SECCOMP_IOCTL_NOTIF_ID_VALID ioctl(2) when you open the /proc/tid/mem file of the target, so you don't accidentally access the memory of the wrong process in the event that the target terminates and its ID gets recycled by some other (unrelated) thread. But plot twist β this ioctl(2) operation is also necessary in other situations too, and here's the tea. π«
Picture this: the supervisor tries to read the pathname argument of a target's blocked mount(2) system call.
- From one of its functions (
func()), the target callsmount(2), which triggers a user-space notification and causes the target to block. π΄ - The supervisor receives the notification, opens
/proc/tid/mem, and (successfully) performs theSECCOMP_IOCTL_NOTIF_ID_VALIDcheck. β - The target receives a signal, which causes the
mount(2)to abort. π - The signal handler executes in the target, and returns.
- Upon return from the handler, the execution of
func()resumes, and it returns (and perhaps other functions are called, overwriting the memory that had been used for the stack frame offunc()). π - Using the address provided in the notification information, the supervisor reads from the target's memory location that used to contain the pathname.
- The supervisor now calls
mount(2)with some arbitrary bytes obtained in the previous step. π
So the conclusion from all that? Since the target's blocked system call may be interrupted by a signal handler, the supervisor must be written to expect that the target may abandon its system call at any time; in such an event, any information the supervisor got from the target's memory must be considered invalid.
To prevent these scenarios, every read from the target's memory must be separated from use of the bytes so obtained by a SECCOMP_IOCTL_NOTIF_ID_VALID check. In the example above, that check would go between the two final steps. And there's an example of such a check in EXAMPLES later. β¨
Following on from all that, it should be clear that a write by the supervisor into the target's memory can never be considered safe. Nope, not even a little. π«
π¦ Caveats regarding blocking system calls π¦
Say the target performs a blocking system call (like accept(2)) that the supervisor is supposed to handle. The supervisor might then turn around and execute that same blocking system call itself.
In this scenario, super important to note: if the target's system call now gets interrupted by a signal, the supervisor is not informed of it. And if the supervisor doesn't take suitable steps to actively discover that the target's system call has been canceled, various difficulties can pop off. Take the accept(2) example β the supervisor might stay blocked in its own accept(2), holding onto a port number that the target (which, after the interruption by the signal handler, maybe closed its listening socket) might expect to be able to reuse in a bind(2) call. Awkward. π¬
So when the supervisor wants to emulate a blocking system call, it has to do it in a way that gets it informed if the target's system call is interrupted by a signal handler. For example, if the supervisor itself executes the same blocking system call, it could employ a separate thread that uses the SECCOMP_IOCTL_NOTIF_ID_VALID operation to check if the target is still blocked in its system call. Alternatively, in the accept(2) example, the supervisor might use poll(2) to monitor both the notification file descriptor (so it discovers when the target's accept(2) call has been interrupted) and the listening file descriptor (so it knows when a connection is available). Two birds, one poll(2). π¦
And if the target's system call does get interrupted, the supervisor must take care to release resources (like file descriptors) that it acquired on behalf of the target. Don't be a litterbug! πΈ
π Interaction with SA_RESTART signal handlers π
Consider this scenario:
- The target process has used
sigaction(2)to install a signal handler with theSA_RESTARTflag. π οΈ - The target has made a system call that triggered a seccomp user-space notification, and the target is currently blocked until the supervisor sends a notification response. π΄
- A signal is delivered to the target and the signal handler is executed.
- When (if) the supervisor attempts to send a notification response, the
SECCOMP_IOCTL_NOTIF_SENDioctl(2)operation will fail with theENOENTerror. π
In this scenario, the kernel will restart the target's system call. Consequently, the supervisor will receive another user-space notification. So depending on how many times the blocked system call gets interrupted by a signal handler, the supervisor may receive multiple notifications for the same instance of a system call in the target. It's giving "infinite loop energy" but make it system calls. π
One oddity: system call restarting as described in this scenario will occur even for the blocking system calls listed in signal(7) that would never normally be restarted by the SA_RESTART flag. Wild, right? π
And furthermore, if the supervisor's response is a file descriptor added with SECCOMP_IOCTL_NOTIF_ADDFD, then the flag SECCOMP_ADDFD_FLAG_SEND can be used to atomically add the file descriptor and return that value, making sure no file descriptors are inadvertently leaked into the target. Clean. Iconic. No leaks. β¨
π BUGS π
If a SECCOMP_IOCTL_NOTIF_RECV ioctl(2) operation is performed after the target terminates, then the ioctl(2) call simply blocks (rather than returning an error to indicate that the target no longer exists). Yeah. It just... sits there. Forever. Ghosted by a dead process fr fr. π»
π EXAMPLES π
The (somewhat contrived) program below demonstrates the use of the interfaces described in this page. The program creates a child process that serves as the "target" process. The child process installs a seccomp filter that returns the SECCOMP_RET_USER_NOTIF action value if a call is made to mkdir(2). The child process then calls mkdir(2) once for each of the supplied command-line arguments, and reports the result returned by the call. After processing all arguments, the child process terminates. π¬
The parent process acts as the supervisor, listening for the notifications that are generated when the target process calls mkdir(2). When such a notification occurs, the supervisor examines the memory of the target process (using /proc/pid/mem) to discover the pathname argument that was supplied to the mkdir(2) call, and performs one of the following actions:
- If the pathname begins with the prefix
"/tmp/", then the supervisor attempts to create the specified directory, and then spoofs a return for the target process based on the return value of the supervisor'smkdir(2)call. In the event that that call succeeds, the spoofed success return value is the length of the pathname. π - π If the pathname kicks off with
"./"β so like, a relative pathname, bestie β the supervisor sends aSECCOMP_USER_NOTIF_FLAG_CONTINUEresponse back to the kernel, basically saying "go off, do your thing," and the kernel then executes the target process'smkdir(2)call. - β¨ But if the pathname starts with literally any other prefix, the supervisor spoofs an error return for the target process, so that target's
mkdir(2)call appears to fail with the errorEOPNOTSUPP("Operation not supported"). And plot twist π«Ά β if the pathname is exactly"/bye", then the supervisor terminates. gone. bye.
This program can be used to demonstrate various aspects of the behavior of the seccomp user-space notification mechanism. To help aid such demonstrations, the program logs various messages showing what the target process is doing (lines prefixed "T:") and what the supervisor is doing (indented lines prefixed "S:").
In the following example, the target attempts to create the directory /tmp/x. The second the supervisor gets the notification, it creates that directory on the target's behalf and spoofs a success return for the target process's mkdir(2) call to receive. teamwork, honestly π
$ ./seccomp_unotify /tmp/x;
T: PID = 23168
T: about to mkdir("/tmp/x")
S: got notification (ID 0x17445c4a0f4e0e3c) for PID 23168
S: executing: mkdir("/tmp/x", 0700)
S: success! spoofed return = 6
S: sending response (flags = 0; val = 6; error = 0)
T: SUCCESS: mkdir(2) returned 6
T: terminating
S: target has terminated; bye
Notice, in the output above, that the spoofed return value the target process sees is 6 (that's the length of the pathname /tmp/x), whereas a normal mkdir(2) call returns 0 on success. iconic little detail, no cap.
Next up: the target tries to create a directory using the relative pathname ./sub. Since that pathname starts with "./", the supervisor sends a SECCOMP_USER_NOTIF_FLAG_CONTINUE response to the kernel, and the kernel then (successfully) executes the target process's mkdir(2) call. slay πΈ
$ ./seccomp_unotify ./sub;
T: PID = 23204
T: about to mkdir("./sub")
S: got notification (ID 0xddb16abe25b4c12) for PID 23204
S: target can execute system call
S: sending response (flags = 0x1; val = 0; error = 0)
T: SUCCESS: mkdir(2) returned 0
T: terminating
S: target has terminated; bye
Now if the target process tries to create a directory with a pathname that doesn't start with "." and doesn't begin with the prefix /tmp/, the supervisor spoofs an error return (EOPNOTSUPP, "Operation not supported") for the target's mkdir(2) call β and that call is not executed. denied. π
$ ./seccomp_unotify /xxx;
T: PID = 23178
T: about to mkdir("/xxx")
S: got notification (ID 0xe7dc095d1c524e80) for PID 23178
S: spoofing error response (Operation not supported)
S: sending response (flags = 0; val = 0; error = -95)
T: ERROR: mkdir(2): Operation not supported
T: terminating
S: target has terminated; bye
In this next one, the target process tries to create a directory with the pathname /tmp/nosuchdir/b. Upon receiving the notification, the supervisor tries to create that directory, but the mkdir(2) call fails because the directory /tmp/nosuchdir does not exist. So, consequently, the supervisor spoofs an error return that passes the error it got straight back to the target process's mkdir(2) call. passing the L along, fr fr π
$ ./seccomp_unotify /tmp/nosuchdir/b;
T: PID = 23199
T: about to mkdir("/tmp/nosuchdir/b")
S: got notification (ID 0x8744454293506046) for PID 23199
S: executing: mkdir("/tmp/nosuchdir/b", 0700)
S: failure! (errno = 2; No such file or directory)
S: sending response (flags = 0; val = 0; error = -2)
T: ERROR: mkdir(2): No such file or directory
T: terminating
S: target has terminated; bye
If the supervisor receives a notification and sees that the argument of the target's mkdir(2) is the string "/bye", then β on top of spoofing an EOPNOTSUPP error β the supervisor terminates. And if the target process then executes another mkdir(2) that triggers its seccomp filter to return the SECCOMP_RET_USER_NOTIF action value, the kernel makes the target process's system call fail with the error ENOSYS ("Function not implemented"). Exhibit A, right here π
$ ./seccomp_unotify /bye /tmp/y;
T: PID = 23185
T: about to mkdir("/bye")
S: got notification (ID 0xa81236b1d2f7b0f4) for PID 23185
S: spoofing error response (Operation not supported)
S: sending response (flags = 0; val = 0; error = -95)
S: terminating **********
T: ERROR: mkdir(2): Operation not supported
T: about to mkdir("/tmp/y")
T: ERROR: mkdir(2): Function not implemented
T: terminating
π Program source π
okay bestie, here's the whole thing, every last line, raw and unfiltered:
#define _GNU_SOURCE
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <linux/audit.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <signal.h>
#include <stdbool.h>
#include <stdcountof.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
#define streq(...) (strcmp(__VA_ARGS__) == 0)
/* Send the file descriptor 'fd' over the connected UNIX domain socket
'sockfd'. Returns 0 on success, or -1 on error. */
static int
sendfd(int sockfd, int fd)
{
int data;
struct iovec iov;
struct msghdr msgh;
struct cmsghdr *cmsgp;
/* Allocate a char array of suitable size to hold the ancillary data.
However, since this buffer is in reality a 'struct cmsghdr', use a
union to ensure that it is suitably aligned. */
union {
char buf[CMSG_SPACE(sizeof(int))];
/* Space large enough to hold an 'int' */
struct cmsghdr align;
} controlMsg;
/* The 'msg_name' field can be used to specify the address of the
destination socket when sending a datagram. However, we do not
need to use this field because 'sockfd' is a connected socket. */
msgh.msg_name = NULL;
msgh.msg_namelen = 0;
/* On Linux, we must transmit at least one byte of real data in
order to send ancillary data. We transmit an arbitrary integer
whose value is ignored by recvfd(). */
msgh.msg_iov = &iov;
msgh.msg_iovlen = 1;
iov.iov_base = &data;
iov.iov_len = sizeof(int);
data = 12345;
/* Set 'msghdr' fields that describe ancillary data */
msgh.msg_control = controlMsg.buf;
msgh.msg_controllen = sizeof(controlMsg.buf);
/* Set up ancillary data describing file descriptor to send */
cmsgp = CMSG_FIRSTHDR(&msgh);
cmsgp->cmsg_level = SOL_SOCKET;
cmsgp->cmsg_type = SCM_RIGHTS;
cmsgp->cmsg_len = CMSG_LEN(sizeof(int));
memcpy(CMSG_DATA(cmsgp), &fd, sizeof(int));
/* Send real plus ancillary data */
if (sendmsg(sockfd, &msgh, 0) == -1)
return -1;
return 0;
}
/* Receive a file descriptor on a connected UNIX domain socket. Returns
the received file descriptor on success, or -1 on error. */
static int
recvfd(int sockfd)
{
int data, fd;
ssize_t nr;
struct iovec iov;
struct msghdr msgh;
/* Allocate a char buffer for the ancillary data. See the comments
in sendfd() */
union {
char buf[CMSG_SPACE(sizeof(int))];
struct cmsghdr align;
} controlMsg;
struct cmsghdr *cmsgp;
/* The 'msg_name' field can be used to obtain the address of the
sending socket. However, we do not need this information. */
msgh.msg_name = NULL;
msgh.msg_namelen = 0;
/* Specify buffer for receiving real data */
msgh.msg_iov = &iov;
msgh.msg_iovlen = 1;
iov.iov_base = &data; /* Real data is an 'int' */
iov.iov_len = sizeof(int);
/* Set 'msghdr' fields that describe ancillary data */
msgh.msg_control = controlMsg.buf;
msgh.msg_controllen = sizeof(controlMsg.buf);
/* Receive real plus ancillary data; real data is ignored */
nr = recvmsg(sockfd, &msgh, 0);
if (nr == -1)
return -1;
cmsgp = CMSG_FIRSTHDR(&msgh);
/* Check the validity of the 'cmsghdr' */
if (cmsgp == NULL
|| cmsgp->cmsg_len != CMSG_LEN(sizeof(int))
|| cmsgp->cmsg_level != SOL_SOCKET
|| cmsgp->cmsg_type != SCM_RIGHTS)
{
errno = EINVAL;
return -1;
}
/* Return the received file descriptor to our caller */
memcpy(&fd, CMSG_DATA(cmsgp), sizeof(int));
return fd;
}
static void
sigchldHandler(int sig)
{
char msg[] = "\tS: target has terminated; bye\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
_exit(EXIT_SUCCESS);
}
static int
seccomp(unsigned int operation, unsigned int flags, void *args)
{
return syscall(SYS_seccomp, operation, flags, args);
}
/* The following is the x86-64-specific BPF boilerplate code for checking
that the BPF program is running on the right architecture + ABI. At
completion of these instructions, the accumulator contains the system
call number. */
/* For the x32 ABI, all system call numbers have bit 30 set */
#define X32_SYSCALL_BIT 0x40000000
#define X86_64_CHECK_ARCH_AND_LOAD_SYSCALL_NR \
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, \
(offsetof(struct seccomp_data, arch))), \
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0, 2), \
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, \
(offsetof(struct seccomp_data, nr))), \
BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, X32_SYSCALL_BIT, 0, 1), \
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS)
/* installNotifyFilter() installs a seccomp filter that generates
user-space notifications (SECCOMP_RET_USER_NOTIF) when the process
calls mkdir(2); the filter allows all other system calls.
The function return value is a file descriptor from which the
user-space notifications can be fetched. */
okay bestie we are so deep in this example rn and this next bit is pure code, so buckle up β no cap, every single line, minus nothing π«Ά
static int
installNotifyFilter(void)
{
int notifyFd;
struct sock_filter filter[] = {
X86_64_CHECK_ARCH_AND_LOAD_SYSCALL_NR,
/* mkdir() triggers notification to user-space supervisor */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_mkdir, 0, 1),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_USER_NOTIF),
/* Every other system call is allowed */
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = countof(filter),
.filter = filter,
};
/* Install the filter with the SECCOMP_FILTER_FLAG_NEW_LISTENER flag;
as a result, seccomp() returns a notification file descriptor. */
notifyFd = seccomp(SECCOMP_SET_MODE_FILTER,
SECCOMP_FILTER_FLAG_NEW_LISTENER, &prog);
if (notifyFd == -1)
err(EXIT_FAILURE, "seccomp-install-notify-filter");
return notifyFd;
}
/* Close a pair of sockets created by socketpair() */
static void
closeSocketPair(int sockPair[2])
{
if (close(sockPair[0]) == -1)
err(EXIT_FAILURE, "closeSocketPair-close-0");
if (close(sockPair[1]) == -1)
err(EXIT_FAILURE, "closeSocketPair-close-1");
}
/* Implementation of the target process. Create a child process that:
(1) installs a seccomp filter with the
SECCOMP_FILTER_FLAG_NEW_LISTENER flag;
(2) writes the seccomp notification file descriptor returned from
the previous step onto the UNIX domain socket, 'sockPair[0]';
(3) calls mkdir(2) for each element of 'argv'.
The function return value in the parent is the PID of the child
process; the child does not return from this function. */
static pid_t
targetProcess(int sockPair[2], char *argv[])
{
int notifyFd, s;
pid_t targetPid;
targetPid = fork();
if (targetPid == -1)
err(EXIT_FAILURE, "fork");
if (targetPid > 0) /* In parent, return PID of child */
return targetPid;
/* Child falls through to here */
printf("T: PID = %ld\n", (long) getpid());
/* Install seccomp filter(s) */
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
err(EXIT_FAILURE, "prctl");
notifyFd = installNotifyFilter();
/* Pass the notification file descriptor to the tracing process over
a UNIX domain socket */
if (sendfd(sockPair[0], notifyFd) == -1)
err(EXIT_FAILURE, "sendfd");
/* Notification and socket FDs are no longer needed in target */
if (close(notifyFd) == -1)
err(EXIT_FAILURE, "close-target-notify-fd");
closeSocketPair(sockPair);
/* Perform a mkdir() call for each of the command-line arguments */
for (char **ap = argv; *ap != NULL; ap++) {
printf("\nT: about to mkdir(\"%s\")\n", *ap);
s = mkdir(*ap, 0700);
if (s == -1)
perror("T: ERROR: mkdir(2)");
else
printf("T: SUCCESS: mkdir(2) returned %d\n", s);
}
printf("\nT: terminating\n");
exit(EXIT_SUCCESS);
}
/* Check that the notification ID provided by a SECCOMP_IOCTL_NOTIF_RECV
operation is still valid. It will no longer be valid if the target
process has terminated or is no longer blocked in the system call that
generated the notification (because it was interrupted by a signal).
This operation can be used when doing such things as accessing
/proc/PID files in the target process in order to avoid TOCTOU race
conditions where the PID that is returned by SECCOMP_IOCTL_NOTIF_RECV
terminates and is reused by another process. */
static bool
cookieIsValid(int notifyFd, uint64_t id)
{
return ioctl(notifyFd, SECCOMP_IOCTL_NOTIF_ID_VALID, &id) == 0;
}
/* Access the memory of the target process in order to fetch the
pathname referred to by the system call argument 'argNum' in
'req->data.args[]'. The pathname is returned in 'path',
a buffer of 'size' bytes allocated by the caller.
Returns true if the pathname is successfully fetched, and false
otherwise. For possible causes of failure, see the comments below. */
static bool
getTargetPathname(struct seccomp_notif *req, int notifyFd,
int argNum, char *path, size_t size)
{
int procMemFd;
char procMemPath[PATH_MAX];
ssize_t nread;
snprintf(procMemPath, sizeof(procMemPath), "/proc/%d/mem", req->pid);
procMemFd = open(procMemPath, O_RDONLY | O_CLOEXEC);
if (procMemFd == -1)
return false;
/* Check that the process whose info we are accessing is still alive
and blocked in the system call that caused the notification.
If the SECCOMP_IOCTL_NOTIF_ID_VALID operation (performed in
cookieIsValid()) succeeded, we know that the /proc/PID/mem file
descriptor that we opened corresponded to the process for which we
received a notification. If that process subsequently terminates,
then read() on that file descriptor will return 0 (EOF). */
if (!cookieIsValid(notifyFd, req->id)) {
close(procMemFd);
return false;
}
/* Read bytes at the location containing the pathname argument */
nread = pread(procMemFd, path, size, req->data.args[argNum]);
close(procMemFd);
if (nread <= 0)
return false;
/* Once again check that the notification ID is still valid. The
case we are particularly concerned about here is that just
before we fetched the pathname, the target's blocked system
call was interrupted by a signal handler, and after the handler
returned, the target carried on execution (past the interrupted
system call). In that case, we have no guarantees about what we
are reading, since the target's memory may have been arbitrarily
changed by subsequent operations. */
if (!cookieIsValid(notifyFd, req->id)) {
perror("\tS: notification ID check failed!!!");
return false;
}
/* Even if the target's system call was not interrupted by a signal,
we have no guarantees about what was in the memory of the target
process. (The memory may have been modified by another thread, or
even by an external attacking process.) We therefore treat the
buffer returned by pread() as untrusted input. The buffer should
contain a terminating null byte; if not, then we will trigger an
error for the target process. */
if (strnlen(path, nread) < nread)
return true;
return false;
}
/* Allocate buffers for the seccomp user-space notification request and
response structures. It is the caller's responsibility to free the
buffers returned via 'req' and 'resp'. */
static void
allocSeccompNotifBuffers(struct seccomp_notif **req,
struct seccomp_notif_resp **resp,
struct seccomp_notif_sizes *sizes)
{
size_t resp_size;
/* Discover the sizes of the structures that are used to receive
notifications and send notification responses, and allocate
buffers of those sizes. */
if (seccomp(SECCOMP_GET_NOTIF_SIZES, 0, sizes) == -1)
err(EXIT_FAILURE, "seccomp-SECCOMP_GET_NOTIF_SIZES");
*req = malloc(sizes->seccomp_notif);
if (*req == NULL)
err(EXIT_FAILURE, "malloc-seccomp_notif");
/* When allocating the response buffer, we must allow for the fact
that the user-space binary may have been built with user-space
headers where 'struct seccomp_notif_resp' is bigger than the
response buffer expected by the (older) kernel. Therefore, we
allocate a buffer that is the maximum of the two sizes. This
ensures that if the supervisor places bytes into the response
structure that are past the response size that the kernel expects,
then the supervisor is not touching an invalid memory location. */
resp_size = sizes->seccomp_notif_resp;
if (sizeof(struct seccomp_notif_resp) > resp_size)
resp_size = sizeof(struct seccomp_notif_resp);
*resp = malloc(resp_size);
if (*resp == NULL)
err(EXIT_FAILURE, "malloc-seccomp_notif_resp");
}
/* Handle notifications that arrive via the SECCOMP_RET_USER_NOTIF file
descriptor, 'notifyFd'. */
static void
handleNotifications(int notifyFd)
{
bool pathOK;
char path[PATH_MAX];
struct seccomp_notif *req;
struct seccomp_notif_resp *resp;
struct seccomp_notif_sizes sizes;
allocSeccompNotifBuffers(&req, &resp, &sizes);
/* Loop handling notifications */
for (;;) {
/* Wait for next notification, returning info in '*req' */
memset(req, 0, sizes.seccomp_notif);
if (ioctl(notifyFd, SECCOMP_IOCTL_NOTIF_RECV, req) == -1) {
if (errno == EINTR)
continue;
err(EXIT_FAILURE, "\tS: ioctl-SECCOMP_IOCTL_NOTIF_RECV");
}
printf("\tS: got notification (ID %#llx) for PID %d\n",
req->id, req->pid);
/* The only system call that can generate a notification event
is mkdir(2). Nevertheless, we check that the notified system
call is indeed mkdir() as kind of future-proofing of this
code in case the seccomp filter is later modified to
generate notifications for other system calls. */
if (req->data.nr != SYS_mkdir) {
printf("\tS: notification contained unexpected "
"system call number; bye!!!\n");
exit(EXIT_FAILURE);
}
pathOK = getTargetPathname(req, notifyFd, 0, path, sizeof(path));
/* Prepopulate some fields of the response */
resp->id = req->id; /* Response includes notification ID */
resp->flags = 0;
resp->val = 0;
/* If getTargetPathname() failed, trigger an EINVAL error
response (sending this response may yield an error if the
failure occurred because the notification ID was no longer
valid); if the directory is in /tmp, then create it on behalf
of the supervisor; if the pathname starts with '.', tell the
kernel to let the target process execute the mkdir();
otherwise, give an error for a directory pathname in any other
location. */
and we are stopping RIGHT there bestie β mid-function, mid-comment, absolute cliffhanger energy π slay, part 7 has the rest π
okay and the code just keeps going, bestie β don't panic, it's still the same supervisor β¨
if (!pathOK) {
resp->error = -EINVAL;
printf("\tS: spoofing error for invalid pathname (%s)\n",
strerror(-resp->error));
} else if (strncmp(path, "/tmp/", strlen("/tmp/")) == 0) {
printf("\tS: executing: mkdir(\"%s\", %#llo)\n",
path, req->data.args[1]);
if (mkdir(path, req->data.args[1]) == 0) {
resp->error = 0; /* "Success" */
resp->val = strlen(path); /* Used as return value of
mkdir() in target */
printf("\tS: success! spoofed return = %lld\n",
resp->val);
} else {
/* If mkdir() failed in the supervisor, pass the error
back to the target */
resp->error = -errno;
printf("\tS: failure! (errno = %d; %s)\n", errno,
strerror(errno));
}
} else if (strncmp(path, "./", strlen("./")) == 0) {
resp->error = resp->val = 0;
resp->flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;
printf("\tS: target can execute system call\n");
} else {
resp->error = -EOPNOTSUPP;
printf("\tS: spoofing error response (%s)\n",
strerror(-resp->error));
}
/* Send a response to the notification */
printf("\tS: sending response "
"(flags = %#x; val = %lld; error = %d)\n",
resp->flags, resp->val, resp->error);
if (ioctl(notifyFd, SECCOMP_IOCTL_NOTIF_SEND, resp) == -1) {
if (errno == ENOENT)
printf("\tS: response failed with ENOENT; "
"perhaps target process's syscall was "
"interrupted by a signal?\n");
else
perror("ioctl-SECCOMP_IOCTL_NOTIF_SEND");
}
/* If the pathname is just "/bye", then the supervisor breaks out
of the loop and terminates. This allows us to see what happens
if the target process makes further calls to mkdir(2). */
if (streq(path, "/bye"))
break;
}
free(req);
free(resp);
printf("\tS: terminating **********\n");
exit(EXIT_FAILURE);
}
/* Implementation of the supervisor process:
(1) obtains the notification file descriptor from 'sockPair[1]'
(2) handles notifications that arrive on that file descriptor. */
static void
supervisor(int sockPair[2])
{
int notifyFd;
notifyFd = recvfd(sockPair[1]);
if (notifyFd == -1)
err(EXIT_FAILURE, "recvfd");
closeSocketPair(sockPair); /* We no longer need the socket pair */
handleNotifications(notifyFd);
}
int
main(int argc, char *argv[])
{
int sockPair[2];
struct sigaction sa;
setbuf(stdout, NULL);
if (argc < 2) {
fprintf(stderr, "At least one pathname argument is required\n");
exit(EXIT_FAILURE);
}
/* Create a UNIX domain socket that is used to pass the seccomp
notification file descriptor from the target process to the
supervisor process. */
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockPair) == -1)
err(EXIT_FAILURE, "socketpair");
/* Create a child process--the "target"--that installs seccomp
filtering. The target process writes the seccomp notification
file descriptor onto 'sockPair[0]' and then calls mkdir(2) for
each directory in the command-line arguments. */
(void) targetProcess(sockPair, &argv[optind]);
/* Catch SIGCHLD when the target terminates, so that the
supervisor can also terminate. */
sa.sa_handler = sigchldHandler;
sa.sa_flags = 0;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGCHLD, &sa, NULL) == -1)
err(EXIT_FAILURE, "sigaction");
supervisor(sockPair);
exit(EXIT_SUCCESS);
}
β¨ SEE ALSO β¨
go read these next, bestie, they're the whole friend group π
and if you want even more example energy, there's another example program hiding in the kernel source file samples/seccomp/user-trap.c π go peek, it's cute fr fr
okay that's literally the whole page, you survived, main character behaviour honestly π this was Linux man-pages 6.20, dated 2026-06-05, and the page you just devoured is seccomp_unotify(2) π¦ go forth and notify responsibly, ily, mwah π
β¨
π¦ where this page comes from
- arch rolling: man-pages 6.20-1 Β· BSD-2-Clause AND BSD-3-Clause AND BSD-4.3TAHOE AND BSD-4-Clause-UC AND GPL-1.0-or-later AND GPL-2.0-only AND GPL-2.0-or-later AND GPL-3.0-or-later AND LGPL-3.0-linking-exception AND LGPL-3.0-or-later AND LicenseRef-Public-Domain AND LicenseRef-UltraPermissive AND Linux-man-pages-1-para AND Linux-man-pages-copyleft AND Linux-man-pages-copyleft-2-para AND Linux-man-pages-copyleft-var AND MIT AND Spencer-94
- nixpkgs unstable: man-pages 6.20
- π the page's license: Linux-man-pages-copyleft
- Copyright 2020, Michael Kerrisk <mtk.manpages@gmail.com>
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms