Skip to content

Instantly share code, notes, and snippets.

@hansent
Created September 3, 2026 20:23
Show Gist options
  • Select an option

  • Save hansent/e0645f1e2cdbac07f0ac9728bdd51bba to your computer and use it in GitHub Desktop.

Select an option

Save hansent/e0645f1e2cdbac07f0ac9728bdd51bba to your computer and use it in GitHub Desktop.
gVisor host UDS reproducer: recvmsg fails on an SCM_RIGHTS-imported socket with SO_PASSCRED enabled (runsc release-20260817.0)

Standalone reproducer (no Kubernetes, no GPU) — Linux only

Requires Python 3 on Linux (SO_PASSCRED / SCM_CREDENTIALS are Linux Unix-socket facilities). One receive case per client invocation.

  1. Configure a dedicated runsc runtime with the two flags in /etc/docker/daemon.json (Docker has no per-run runtime-flag option; see Docker's "alternative runtimes" documentation), then restart Docker:
{
  "runtimes": {
    "runsc-uds-repro": {
      "path": "/usr/local/bin/runsc",
      "runtimeArgs": ["--host-uds=open", "--directfs=false"]
    }
  }
}
  1. Start the server on the host (leave it running):
mkdir -p /tmp/uds-repro && python3 uds_passcred_repro.py server /tmp/uds-repro
  1. Run each case under explicit runc (control) and under the runsc runtime:
for c in recv recvmsg-creds recvmsg-creds-rights; do
  docker run --rm --runtime=runc -v /tmp/uds-repro:/uds -v "$PWD":/r:ro python:3.11-slim python3 /r/uds_passcred_repro.py client /uds $c
done
for c in recv recvmsg-creds recvmsg-creds-rights; do
  docker run --rm --runtime=runsc-uds-repro -v /tmp/uds-repro:/uds -v "$PWD":/r:ro python:3.11-slim python3 /r/uds_passcred_repro.py client /uds $c
done
  1. Control without SO_PASSCRED on the passed socket: stop the server (Ctrl-C), restart it with --no-passcred, and repeat step 3 for the runsc runtime.

--directfs=false is needed only so that the sandbox's own SCM_CREDENTIALS hello is accepted by the server (see the companion credentials issue); it does not affect the receive behaviour reported here. The script unlinks its socket on start, so stale sockets from earlier runs are harmless.

#!/usr/bin/env python3
"""Reproducer: recvmsg fails on an SCM_RIGHTS-imported Unix socket that has SO_PASSCRED enabled (gVisor).
Linux only (SO_PASSCRED / SCM_CREDENTIALS). One receive case per client invocation, so SOCK_STREAM framing cannot
mix the cases:
server: python3 uds_passcred_repro.py server DIR [--no-passcred]
client: python3 uds_passcred_repro.py client DIR {recv,recvmsg-creds,recvmsg-creds-rights}
Per client connection the server: accepts on DIR/sock (SOCK_SEQPACKET, SO_PASSCRED on the listener), reads the
client's "hello:<case>" (with SCM_CREDENTIALS), creates ONE SOCK_STREAM socketpair, enables SO_PASSCRED on both ends
(unless --no-passcred), passes one end via SCM_RIGHTS, waits for "hi" on it, then sends exactly ONE message on it:
recv / recvmsg-creds -> b"payload" (no ancillary data attached by the server; the kernel adds SCM_CREDENTIALS
to the receiver if the socket has SO_PASSCRED)
recvmsg-creds-rights -> b"payload" with one pipe write-end attached via SCM_RIGHTS; the server then reports
whether the client wrote through that pipe.
The client receives that one message with the selected shape and prints OK or the error.
"""
import argparse, array, os, select, socket, struct, sys
CASES = ("recv", "recvmsg-creds", "recvmsg-creds-rights")
def serve(dirpath, passcred):
path = os.path.join(dirpath, "sock")
try: os.unlink(path)
except FileNotFoundError: pass
srv = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET); srv.bind(path); os.chmod(path, 0o777); srv.listen(4)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_PASSCRED, 1)
print(f"server: listening on {path}, SO_PASSCRED on passed sockets = {passcred}", flush=True)
while True:
c, _ = srv.accept(); c.setsockopt(socket.SOL_SOCKET, socket.SO_PASSCRED, 1)
try:
msg, anc, _, _ = c.recvmsg(256, socket.CMSG_SPACE(64))
creds = [struct.unpack("3i", d[:12]) for lvl, typ, d in anc if typ == socket.SCM_CREDENTIALS]
case = msg.decode(errors="replace").split(":", 1)[-1]
print(f"server: client case={case!r} kernel creds (pid, uid, gid) = {creds}", flush=True)
a, b = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
if passcred:
a.setsockopt(socket.SOL_SOCKET, socket.SO_PASSCRED, 1); b.setsockopt(socket.SOL_SOCKET, socket.SO_PASSCRED, 1)
c.sendmsg([b"FD"], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array("i", [b.fileno()]))]); b.close()
a.settimeout(10); print(f"server: passed a SOCK_STREAM socket, got {a.recv(16)!r} on it", flush=True)
if case == "recvmsg-creds-rights":
r, w = os.pipe()
a.sendmsg([b"payload"], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array("i", [w]))]); os.close(w)
rl, _, _ = select.select([r], [], [], 5)
print("server: sent payload + SCM_RIGHTS pipe; pipe read:", os.read(r, 64) if rl else b"<timeout: client did not write>", flush=True); os.close(r)
else:
a.send(b"payload"); print("server: sent payload (no ancillary data from the server)", flush=True)
a.close()
except Exception as e: print("server: error", type(e).__name__, e, flush=True)
finally: c.close(); print("server: --- done ---", flush=True)
def run_client(dirpath, case):
s = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET); s.connect(os.path.join(dirpath, "sock"))
s.sendmsg([f"hello:{case}".encode()], [(socket.SOL_SOCKET, socket.SCM_CREDENTIALS, struct.pack("3i", os.getpid(), os.getuid(), os.getgid()))])
print(f"client[{case}]: uid={os.getuid()} sent SCM_CREDENTIALS ok", flush=True)
s.settimeout(10); _, anc, _, _ = s.recvmsg(16, socket.CMSG_SPACE(64))
fd = [x for lvl, typ, d in anc if typ == socket.SCM_RIGHTS for x in array.array("i", d).tolist()][0]
ps = socket.socket(fileno=fd); ps.settimeout(6)
print(f"client[{case}]: received passed socket fd={fd} type={ps.type} SO_PASSCRED as seen here={ps.getsockopt(socket.SOL_SOCKET, socket.SO_PASSCRED)}", flush=True)
ps.send(b"hi")
try:
if case == "recv":
m, anc2 = ps.recv(64), []
elif case == "recvmsg-creds":
m, anc2, _, _ = ps.recvmsg(64, 4096, 0)
else:
m, anc2, _, _ = ps.recvmsg(64, 4096, socket.MSG_CMSG_CLOEXEC)
fds = [x for lvl, typ, d in anc2 if typ == socket.SCM_RIGHTS for x in array.array("i", d).tolist()]
print(f"client[{case}]: OK msg={m!r} cmsg_types={[typ for _, typ, _ in anc2]} passed_fds={fds}", flush=True)
for x in fds: os.write(x, b"written-through-passed-pipe"); os.close(x)
except OSError as e:
print(f"client[{case}]: FAILED {type(e).__name__}: [Errno {e.errno}] {e.strerror}", flush=True); sys.exit(1)
finally: ps.close(); s.close()
if __name__ == "__main__":
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = p.add_subparsers(dest="mode", required=True)
ps_ = sub.add_parser("server"); ps_.add_argument("dir"); ps_.add_argument("--no-passcred", action="store_true", help="do not enable SO_PASSCRED on the passed socket (control)")
pc = sub.add_parser("client"); pc.add_argument("dir"); pc.add_argument("case", choices=CASES)
args = p.parse_args()
serve(args.dir, not args.no_passcred) if args.mode == "server" else run_client(args.dir, args.case)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment