|
#!/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) |