seccomp User Notification Delegates Selected Syscalls to a Supervisor
A seccomp filter can stop a selected system call before the kernel executes it and emit a notification to a user-space supervisor instead. The target thread remains blocked while the supervisor receives the event and returns a disposition. This behavior turns a filter result into a controlled handoff across the kernel/user-space boundary.
The mechanism is SECCOMP_RET_USER_NOTIF. It differs from ordinary seccomp actions because the BPF filter does not finish the decision by itself. A listener file descriptor becomes the coordination point for notification receipt, response delivery, and optional file-descriptor injection.
The listener belongs to the filter
A process installs a seccomp filter with SECCOMP_FILTER_FLAG_NEW_LISTENER. On success, seccomp() returns a listener file descriptor associated with that filter. A rule that returns SECCOMP_RET_USER_NOTIF causes matching calls to generate events on this descriptor.
int notify_fd = seccomp(
SECCOMP_SET_MODE_FILTER,
SECCOMP_FILTER_FLAG_NEW_LISTENER,
&prog
);The descriptor is tied to the filter rather than to one task. If tasks that share the filter produce matching calls, their notifications arrive through the same listener. The descriptor can also be transferred to another process, for example with SCM_RIGHTS, allowing a separate supervisor to own the handling loop.
Only one listener created with SECCOMP_FILTER_FLAG_NEW_LISTENER can be installed for a thread. This constraint makes listener ownership part of filter construction rather than an unlimited side channel that can be attached repeatedly.
A matching syscall sleeps before execution
For a matching call, SECCOMP_RET_USER_NOTIF does not mean “execute and report.” The call has not yet executed. The target is blocked in the kernel while a notification is made available to the supervisor.
The receive path uses SECCOMP_IOCTL_NOTIF_RECV:
struct seccomp_notif req = {0};
if (ioctl(notify_fd, SECCOMP_IOCTL_NOTIF_RECV, &req) == -1) {
/* handle error */
}req.data carries the syscall number, architecture value, instruction pointer, and register argument values represented by struct seccomp_data. The notification also carries an identifier and the triggering task’s PID as visible from the listener’s PID namespace.
The supervisor eventually sends a struct seccomp_notif_resp with SECCOMP_IOCTL_NOTIF_SEND. It can supply a return value or error, causing the target to resume as if its call had produced that result.
target kernel supervisor
| syscall | |
|-------------------->| |
| | USER_NOTIF event |
| blocked |----------------------->|
| | | decide
| |<-----------------------|
|<---------------------| response |
| resumes | |This split permits a more privileged process to perform an operation on behalf of a restricted target without granting the target the same ambient privilege.
Notification data does not snapshot pointed-to memory
System-call arguments in struct seccomp_data are register values. A pointer argument is therefore an address, not an immutable copy of the bytes stored at that address.
That distinction is a critical boundary. A supervisor that reads a pathname or another pointed-to object from target memory is observing mutable process memory outside the seccomp notification structure. Other threads in the target can modify that memory while the triggering thread is blocked.
The user-notification interface is consequently unsuitable as a stand-alone security-policy engine for decisions that depend on dereferenced target memory. In particular, approving a call and then using SECCOMP_USER_NOTIF_FLAG_CONTINUE creates a time-of-check/time-of-use interval: memory inspected by the supervisor can differ from memory consumed later by the kernel.
The kernel seccomp filter itself avoids this class of dereference because classic seccomp BPF operates on the syscall metadata supplied in struct seccomp_data; it cannot follow arbitrary user pointers.
CONTINUE resumes the original kernel path
A response can set SECCOMP_USER_NOTIF_FLAG_CONTINUE. In that case, the original syscall proceeds in the kernel rather than receiving a synthetic result from the supervisor.
This option is useful when the supervisor decides that the normal kernel path should handle the operation. It is not equivalent to an atomic authorization of mutable pointer contents. The target may have changed relevant memory between supervisor inspection and kernel consumption.
A sound design therefore treats CONTINUE as delegation back to normal kernel enforcement, not as a replacement for kernel-enforced access control. The user-notification mechanism is intended for mediation scenarios such as privileged service on behalf of a less-privileged process.
Notification identifiers close a lifetime race
Each event carries an id. The supervisor copies that identifier into its response. A target can disappear while a notification is being processed, so the interface also provides SECCOMP_IOCTL_NOTIF_ID_VALID to test whether an identifier still refers to a live request.
__u64 id = req.id;
if (ioctl(notify_fd, SECCOMP_IOCTL_NOTIF_ID_VALID, &id) == 0) {
/* request is still valid */
}Validation does not freeze the target’s address space. Its role is narrower: it lets the supervisor detect that the notification itself has become stale before performing work that assumes the request still exists.
The distinction matters for operations with external effects. A supervisor that performs an expensive or privileged action after the request has vanished can create state that no target call will receive.
ADDFD transfers an open file description into the target
Some intercepted operations naturally produce a file descriptor. Returning only an integer is insufficient because the target must receive a real descriptor referring to an open file description.
SECCOMP_IOCTL_NOTIF_ADDFD lets the supervisor install a descriptor into the target associated with a pending notification. SECCOMP_ADDFD_FLAG_SEND can combine descriptor installation with the notification response, making the injected descriptor number the return value delivered to the target.
This mechanism avoids reconstructing descriptor state through a numeric fiction. The kernel performs the descriptor-table insertion, preserving the normal semantics of an actual file descriptor in the target process.
Filter precedence still applies
User notification participates in the normal seccomp action-precedence rules. If another installed filter produces an action with higher precedence than SECCOMP_RET_USER_NOTIF, the supervisor is not notified for that call.
A listener therefore does not sit outside the seccomp filter stack. It is one possible result within that stack. Existing kill, trap, errno, trace, log, and allow behavior continues to be governed by seccomp’s action ordering.
If the listener is absent when a filter returns SECCOMP_RET_USER_NOTIF, the call fails with ENOSYS. The notification path depends on both the filter result and a live attached listener.
Delegation is the durable abstraction
SECCOMP_RET_USER_NOTIF is best modeled as syscall delegation, not user-space emulation of the entire syscall layer. The target stops at a kernel boundary, the supervisor receives bounded syscall metadata, and a response determines whether the target sees a synthetic result, continues through the kernel, or receives a descriptor installed by the kernel.
The hard boundary is equally important: pointed-to process memory remains mutable, notification lifetime can end asynchronously, and filter precedence remains active. Those constraints keep the mechanism useful for privileged mediation without turning it into an implicit substitute for kernel security policy.