Skip to content
LinuxDeep Dive Published Updated 6 min readViews unavailable

Linux seccomp User Notifications: Supervisor Lifecycles Without a False Sandbox

Operate seccomp notification listener file descriptors, request IDs, replies, and recovery while respecting the mechanism's TOCTOU security limits.

Seccomp user notification lets a filter pause a selected system call and notify a userspace supervisor. It is useful when a compatibility layer or container runtime must perform a bounded operation on behalf of a target that cannot perform it directly. It is not a general syscall proxy and must not be presented as a security-policy engine: the Linux manual explicitly warns that inspecting target memory and then allowing the target syscall creates time-of-check/time-of-use races.

This interface is a protocol built around a listener file descriptor. A filter returns SECCOMP_RET_USER_NOTIF; the filter is installed with SECCOMP_FILTER_FLAG_NEW_LISTENER; the resulting FD is read with notification ioctls and may be passed to another process. The notification identifies the syscall metadata and an ID that must be carried into the response. The target blocks until the supervisor replies, the request becomes invalid, or the relevant process/listener lifecycle ends.

Install and hand off the listener deliberately

The filter is still seccomp-BPF. It sees the syscall number, architecture, instruction pointer, and raw argument values described by struct seccomp_data; it cannot safely dereference an arbitrary target pointer to inspect a pathname. The supervisor receives more context than the filter, but that does not make pointed-to memory stable or authorize an operation.

SECCOMP_FILTER_FLAG_NEW_LISTENER causes a successful filter installation to return a close-on-exec listener FD. The flag and SECCOMP_RET_USER_NOTIF require kernel support; check feature availability and handle EINVAL, ENOSYS, permission failures, and unsupported actions separately. Only one filter using NEW_LISTENER can be installed for a thread, and the listener is tied to the filter, not to a single target thread. Forked tasks that inherit that filter can generate requests on the same listener.

If a supervisor is a separate process, transfer the FD through a controlled mechanism such as a Unix-domain socket with SCM_RIGHTS, or arrange inheritance before applying close-on-exec boundaries. Treat the FD as a capability: the receiver can delay or answer intercepted calls. Authenticate the intended receiver and close redundant copies so shutdown produces a predictable end-of-stream condition. Do not make an untrusted application the supervisor for its own restricted process and then treat the resulting answer as independent enforcement.

Negotiate structures and match each request ID

The notification structures can grow. Query SECCOMP_GET_NOTIF_SIZES and allocate the kernel-reported sizes instead of assuming the compiling machine’s header exactly matches the running kernel. Zero structures before SECCOMP_IOCTL_NOTIF_RECV, then validate the request ID, flags, architecture, syscall number, and arguments before applying policy. The API does not return a separate structure-length field; use the reported sizes for allocation, and use SECCOMP_IOCTL_NOTIF_ID_VALID to check whether a request ID is still live before replying. The pid field can be zero when the task is in a PID namespace not visible from the listener’s namespace; do not use it as a global identity without corroboration.

The basic exchange is:

target syscall -> filter returns USER_NOTIF -> kernel queues notification(id)
supervisor: ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, request)
supervisor: inspect only bounded metadata; choose a documented response
supervisor: ioctl(listener, SECCOMP_IOCTL_NOTIF_ID_VALID, request.id)
supervisor: ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, response with same id)

This is a protocol outline, not compilable C. The response ID must correspond to the request being answered. A target can exit, receive a signal, or otherwise invalidate the request while the supervisor is working; handle ENOENT or an invalid ID as a normal race rather than replying to a different event. Do not keep an unbounded queue of requests while the target threads are blocked. Apply backpressure, time limits appropriate to the workload, and a defined error/termination response when the supervisor is shutting down.

SECCOMP_IOCTL_NOTIF_ADDFD can install a supervisor-owned descriptor in the target process, with documented flags controlling the operation. This is a capability transfer, not a pathname authorization primitive. Validate that the source FD is the intended object, use close-on-exec behavior where appropriate, and define who closes the installed descriptor. The ADDFD_FLAG_SEND variant can combine descriptor injection with a response, but feature-detect it and test against the minimum kernel you support.

Why CONTINUE is not a safe path policy

The SECCOMP_USER_NOTIF_FLAG_CONTINUE response asks the kernel to proceed with the original syscall. It does not freeze the target’s address space while the supervisor examines it. If the supervisor reads a pathname from a pointer and then says “continue,” the target can change the pointed-to bytes before the kernel consumes them. A policy that approved /safe/file may therefore allow a different path.

For the same reason, a supervisor should not decide “this openat() is allowed” based on a copied pathname and then use CONTINUE as though the inspected bytes were the kernel’s final input. Use a kernel mechanism designed for the policy boundary, such as Landlock, mount and user namespaces, or an appropriate LSM, and use seccomp to reduce syscall surface. User notification can be a compatibility broker when the supervisor performs the operation itself using validated inputs and returns a carefully defined result; that is a different trust model from authorizing the target’s later syscall.

Keep responses narrow. Returning a fabricated success value for an operation the supervisor did not perform can leave the target’s state inconsistent. Returning an errno should match a tested fallback path. If the supervisor performs a filesystem or device operation itself, it must apply its own identity checks, path resolution, permissions, and resource limits; it does not inherit the target’s credentials merely because the target triggered the notification.

Operational test plan

In a disposable VM, run a small target that requests one harmless syscall covered by a test filter. Confirm listener creation, one received notification, matching response ID, and target completion. Then add tests for two concurrent target threads, a target exiting after notification, a signal during RECV, a supervisor restart, an inherited filter after fork(), a descriptor transfer failure, and a request backlog. Record kernel release, architecture, CONFIG_SECCOMP_FILTER, seccomp action availability, and structure sizes; do not infer runtime support from headers alone.

For path-related experiments, include a race fixture that changes the target buffer after notification. The expected result should demonstrate why a read-then-CONTINUE policy is unsafe, not claim that the notification mechanism closes the race. Keep actual security policy in kernel-enforced controls and use the supervisor for explicit, bounded compatibility work. Observe blocked duration, queue depth, invalidated requests, supervisor restarts, errno classes, and injected-FD counts without logging sensitive argument contents.

The maintainable design is a small filter plus a trusted, observable, restart-safe broker. It must not confuse richer userspace context with a stronger authorization guarantee than the kernel interface provides.

Related:

Sources:

Comments