seccomp User Notifications Move Selected System Calls to a Supervisor

A seccomp filter can do more than allow a system call or reject it in the kernel. When a filter returns SECCOMP_RET_USER_NOTIF, Linux can suspend the calling thread and deliver a description of that attempted system call to a user-space supervisor.

The mechanism creates an interposition boundary around selected calls. It is useful when a less-privileged process needs an operation mediated by another process, such as a container manager handling a call that the container cannot perform directly. The boundary is deliberately narrower than a general security-policy engine: notification state can race with mutable target memory, and the kernel documentation warns against treating the supervisor’s inspection as an authorization primitive.

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.

int listener = seccomp(SECCOMP_SET_MODE_FILTER,
                       SECCOMP_FILTER_FLAG_NEW_LISTENER,
                       &prog);

The BPF program chooses which system calls produce notifications by returning SECCOMP_RET_USER_NOTIF. Calls matched by other actions keep the semantics of those actions.

The listener can be transferred to a supervisor through normal file-descriptor passing, including SCM_RIGHTS over a UNIX domain socket. The filter is the relevant object: if a task using that filter forks, notifications from descendants that retain the filter can arrive through the same listener.

That property separates notification routing from a single process identifier. The supervisor watches the filter’s event stream rather than attaching an independent interception channel to each task.

A notified call waits for a response

When a matched system call reaches the filter, the kernel does not execute it immediately. The calling thread blocks while a notification becomes available on the listener.

The supervisor receives the event with SECCOMP_IOCTL_NOTIF_RECV:

struct seccomp_notif *req;

/* req is allocated using sizes obtained from SECCOMP_GET_NOTIF_SIZES. */
if (ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, req) == -1) {
    /* handle error */
}

The request includes a notification ID, the triggering task identifier as visible from the listener’s PID namespace, and a struct seccomp_data containing the system-call number, architecture value, instruction pointer, and raw argument values.

The notification ID ties a later response to this particular pending operation. A supervisor can check an ID with SECCOMP_IOCTL_NOTIF_ID_VALID before acting on state that may have become stale.

Structure sizes are queried with SECCOMP_GET_NOTIF_SIZES rather than assumed from the headers used to build the supervisor. The kernel interface permits these notification structures to grow, so allocating from the reported sizes avoids turning a build-time layout into a runtime ABI assumption.

Raw arguments are not stable pointed-to data

struct seccomp_data captures register-level system-call arguments. If an argument is a pointer, the notification contains the pointer value, not an immutable copy of the target memory behind it.

Consider a pathname argument. A supervisor may inspect memory from the target to obtain the string, but another thread in that target can modify the memory while the notified thread is blocked. A decision based on one read does not freeze the bytes that a later kernel execution would consume.

This distinction is central to SECCOMP_USER_NOTIF_FLAG_CONTINUE. That response tells the kernel to continue the original system call. Between supervisor inspection and continuation, mutable pointed-to data can change. The seccomp user-notification documentation therefore treats this path as subject to time-of-check/time-of-use races and explicitly rejects it as a basis for security-policy decisions.

The safe interpretation is narrower: the supervisor can mediate operations when another security boundary already constrains the target and when any copied target data is handled with its mutability in mind.

Responses can emulate results or continue execution

A supervisor sends a struct seccomp_notif_resp with SECCOMP_IOCTL_NOTIF_SEND. The response carries the same notification ID and can provide a return value or an error without executing the original system call in the target.

struct seccomp_notif_resp resp = {
    .id = req->id,
    .val = -1,
    .error = EPERM,
    .flags = 0,
};

ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp);

A different response can set SECCOMP_USER_NOTIF_FLAG_CONTINUE, asking the kernel to execute the target’s original call. That path retains normal kernel handling after the notification boundary, but it also carries the mutable-argument race described above.

Emulation has a different constraint. If the operation is supposed to produce a file descriptor, merely returning an integer does not create a corresponding open descriptor in the target. Linux provides SECCOMP_IOCTL_NOTIF_ADDFD so the supervisor can install a file descriptor into the target’s descriptor table. With SECCOMP_ADDFD_FLAG_SEND, descriptor injection and the notification response can be combined atomically from the interface’s point of view.

This makes operations that return descriptors materially different from scalar-return emulation. The result is kernel-managed descriptor state, not just a number placed in a return register.

Filter precedence still applies

User notification is one possible seccomp action, not an override for every filter attached to a task. If another applicable filter returns an action with higher precedence, the listener does not receive the call as a user notification.

That matters when filters are stacked. A supervisor cannot assume that installing a listener guarantees visibility into every call its own BPF program marks with SECCOMP_RET_USER_NOTIF. The combined seccomp policy determines the action that wins.

The architecture field also remains part of correct syscall filtering. System-call numbers are not sufficient by themselves on architectures that expose multiple calling conventions. A filter that reasons about syscall numbers must account for the architecture value before assigning notification behavior.

The boundary is mediation, not authorization

Seccomp user notifications shift selected system-call handling from an in-kernel filter decision to a request-response protocol with a user-space process. That enables emulation, descriptor injection, and controlled continuation without converting the supervisor into part of the kernel.

The same boundary explains its limit. Register values are captured, pointed-to memory can remain mutable, filters can interact through precedence, and a target waits while the supervisor handles each pending request. Those properties fit system-call mediation where the surrounding security model is already established.

Treating the mechanism as a general authorization layer changes the assumption that makes it useful. The listener is a controlled interposition channel; it is not a transaction that freezes every piece of target state until the supervisor replies.