Conversation
…ed sockets getpeername(2) was always answered by writing the sockaddr into the workload's address space from the broker. On kernels without SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV the broker runs in LegacyReadOnly mode, where that cross-process write fails closed with EOPNOTSUPP, so getpeername failed for every socket. For SocketState::Local and SocketState::AcceptedLocal the descriptor is genuinely connected to the recorded peer, so the kernel's own answer is identical to the broker's. Respond with SECCOMP_USER_NOTIF_FLAG_CONTINUE for those states and let the kernel write the sockaddr in the workload's own address space. That needs no cross-process task-memory write and therefore works on kernels before 5.19. SocketState::Connected keeps the existing substitution. Those descriptors are connected to a loopback relay rather than to the destination the workload requested, so the kernel's answer would leak the relay address. A regression test pins that distinction. This is the path libuv-based runtimes depend on: libuv passes a null peer address to accept4(2) and resolves the peer lazily through getpeername(2) when the application reads remoteAddress. Refs NVIDIA#4058 Signed-off-by: Russell Bryant <rbryant@redhat.com>
…E_RECV Kernels before Linux 5.19 lack SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV, so the broker cannot hold a notified workload thread in a kill-only wait and cannot safely write into workload memory. It fails closed with EOPNOTSUPP on every mediated syscall whose result is an output buffer. accept(fd, &addr, &len) has exactly that shape, so server workloads on RHEL 9.x and RHCOS see EOPNOTSUPP where they expect a connection. Add openshell-accept-shim, a freestanding preloadable library that rewrites an address-bearing accept into accept4(fd, NULL, NULL, flags) followed by getpeername on the accepted descriptor. The first call has no output buffer, so the broker injects the descriptor without touching workload memory; the second is answered with SECCOMP_USER_NOTIF_FLAG_CONTINUE, so the kernel writes the address into the caller's own buffer. Because the workload writes its own memory, the cross-process TOCTOU that WAIT_KILLABLE_RECV exists to close does not apply. The sandbox installs the object at /run/openshell-compat only when the listener reports writes disabled, and composes LD_PRELOAD so a workload's own value is preserved. Installation failure is non-fatal and logged as an OCSF config state change; the sandbox keeps the existing fail-closed behavior. Landlock gates open independently of the file mode, so a world-readable object is not by itself reachable: without an explicit admission the loader reports "cannot open shared object file" and silently drops the shim. Workload children are launched from more than one place -- the sandbox entrypoint and sandbox exec through the boundary -- and each prepares Landlock with its own runtime paths. Admit the object and its directory inside prepare_child_sandbox, the single production entry point, so a launch path cannot set LD_PRELOAD without also letting the loader open what it points at. Resolve the shim's C compiler through the cc crate so the standard CC_<target>, TARGET_CC, and CC overrides apply, along with the per-target wrappers cargo-zigbuild installs when release binaries are cross-compiled from a non-Linux host. Verify the emitted object's ELF machine against CARGO_CFG_TARGET_ARCH and fail the build on a mismatch: a misresolved compiler otherwise produces a valid object for the wrong architecture, which the loader reports as "cannot open shared object file" and is indistinguishable from a policy denial. This is a compatibility aid, not a security control. Nothing in OpenShell trusts its output: loopback and authorization decisions continue to use the kernel's peer address from the broker's own accept4. A workload that unsets LD_PRELOAD, links statically, or issues raw syscalls gains nothing it did not already have. Coverage follows from LD_PRELOAD interposing symbols rather than syscalls: Bun and CPython are covered, Node.js does not need it because libuv passes a null address and resolves the peer lazily through getpeername, and Go and statically linked binaries remain unsupported for address-bearing accept. getpeername is fixed for every runtime because the kernel answers it. The object is built freestanding with -nostdlib so one build per architecture loads under both glibc and musl, carries no DT_NEEDED entry and no text relocations, and leaves __errno_location as its sole undefined symbol. Add an e2e test that asserts what the workload observes rather than how the sandbox arranges it: a listener accepts a connection from a client in the same sandbox and must report the client's address, not EOPNOTSUPP and not its own. It passes unchanged on both kernel generations. Signed-off-by: Russell Bryant <rbryant@redhat.com>
russellb
requested review from
a team,
derekwaynecarr,
mrunalp and
sjenning
as code owners
October 1, 2026 22:53
Signed-off-by: Russell Bryant <rbryant@redhat.com>
Signed-off-by: Russell Bryant <rbryant@redhat.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On kernels before Linux 5.19 the seccomp broker cannot write into workload memory, so it fails closed with
EOPNOTSUPPon any mediated syscall whose result is an output buffer.accept(fd, &addr, &len)has exactly that shape, which makes server workloads unusable on RHEL 9.x and RHCOS. This restores them by rewriting the address-bearing call into a form the broker can satisfy, and fixesgetpeernameon directly connected sockets for every runtime.Related Issue
Closes #4058
Changes
openshell-accept-shim, a freestanding preloadable object that rewrites an address-bearingacceptintoaccept4(fd, NULL, NULL, flags)followed bygetpeernameon the accepted descriptor. The first call has no output buffer, so the broker injects the descriptor without touching workload memory; the second is answered withSECCOMP_USER_NOTIF_FLAG_CONTINUE, so the kernel writes the address into the caller's own buffer. The cross-process TOCTOU thatWAIT_KILLABLE_RECVexists to close does not apply, because the workload writes its own memory./run/openshell-compatonly when the listener reports writes disabled, composingLD_PRELOADso a workload's own value is preserved. Installation failure is non-fatal and logged as an OCSF config state change; the existing fail-closed behavior is retained.prepare_child_sandbox. Landlock gatesopenindependently of the file mode, so a world-readable object is not by itself reachable — the loader reportscannot open shared object fileand silently drops the shim. Workload children are launched from two places (the sandbox entrypoint andsandbox execthrough the boundary), each preparing Landlock with its own runtime paths; admitting at the single shared entry point means a launch path cannot setLD_PRELOADwithout also letting the loader open what it points at.getpeernamefrom the kernel for directly connected sockets, rather than substituting the socket the broker knows about.cccrate soCC_<target>,TARGET_CC, andCCapply, along with the per-target wrapperscargo-zigbuildinstalls when release binaries are cross-compiled from a non-Linux host. Verify the emitted object's ELF machine againstCARGO_CFG_TARGET_ARCHand fail the build on a mismatch — a misresolved compiler otherwise produces a valid object for the wrong architecture, which the loader reports identically to a policy denial.Scope and limits
This is a compatibility aid, not a security control. Nothing in OpenShell trusts its output: loopback and authorization decisions continue to use the kernel's peer address from the broker's own
accept4. A workload that unsetsLD_PRELOAD, links statically, or issues raw syscalls gains nothing it did not already have.Coverage follows from
LD_PRELOADinterposing symbols rather than syscalls. Bun and CPython are covered. Node.js does not need the shim — libuv passes a null address and resolves the peer lazily throughgetpeername. Go and statically linked binaries remain unsupported for address-bearingaccept.getpeernameis fixed for every runtime because the kernel answers it.Testing
mise run pre-commitpasses.Unit and integration tests run on Linux as a non-root user: 191 passed across
openshell-sandboxandopenshell-accept-shim. One pre-existing failure,canonical_tty_environment_replaces_supervisor_identity_defaults, is unrelated to this change — it fails in a bare container whenever the running uid has no/etc/passwdentry, on this branch and without it alike.New coverage:
shim_behavior.rsasserts the object's ELF invariants (noDT_NEEDED, noTEXTREL,__errno_locationas the sole undefined symbol, exactly two exported functions) and the rewrite's behavior.preloaded_shim_remains_readable_after_landlock_for_non_root_workloadforks, drops privileges, enforces Landlock, and confirms the loader can still open the object while an unadmitted neighbor stays denied.runtime_read_only_composition_admits_caller_paths_and_the_shimpins the composition so a launch path cannot drop the admission.End-to-end verification on affected hardware:
e2e/rust/tests/peer_address.rswas run against an OpenShift cluster on RHCOS 9.6 (kernel5.14.0-570.141.1.el9_6.x86_64, noWAIT_KILLABLE_RECV), via the Kubernetes compute driver with an image built from this branch. It passes. The same test fails on that cluster without the change, witherrno 95andld.so: object '/run/openshell-compat/accept_shim.so' from LD_PRELOAD cannot be preloaded. The test asserts what the workload observes rather than how the sandbox arranges it, so it passes unchanged on both kernel generations.Checklist