Wren Linux

wren-linux / bin / no-abstract-sockets

#!/bin/bash
# no-abstract-sockets COMMAND [ARGS...]: written by Wren Linux (parts/incognito.sh
# and the browser and Firefox sandboxes install it as root, under /usr/local/libexec).
# Runs COMMAND so that it (and everything it starts) can't connect to an
# "abstract" Unix socket made by any program outside it. Those sockets have no
# owner or permissions: any user on the computer can connect to one. XWayland
# listens on one (@/tmp/.X11-unix/X0), and anyone who connects to X11 can read
# every X11 window, watch the keys typed into them and type for you. The
# kernel's Landlock (Linux 6.12 or newer) is what stops it. Nothing else is
# restricted: files, the network and ordinary sockets work as before, and
# COMMAND can still make and use abstract sockets of its own.
# Stops with an error if the kernel can't do it, unless
# WREN_ALLOW_ABSTRACT_SOCKETS=1 is set (then COMMAND runs unrestricted).
# Landlock asks for no_new_privs first, or for the rights to do without it. With
# no_new_privs, AppArmor lets a confined program start nothing under another
# profile, and Tor Browser's profile starts Tor that way. So --userns gets those
# rights instead: COMMAND runs in a new user namespace where only this user's own
# numbers exist. Root isn't one of them, so a setuid program or a file with
# capabilities gives nothing there either, and the rights end at the exec. Root's folder
# for X11 sockets has no owner there, and XWayland refuses it: COMMAND gets a folder of
# its own over /tmp/.X11-unix, which only it and what it starts can see.
set -euo pipefail
[ $# -ge 1 ] || { echo "usage: no-abstract-sockets [--userns] COMMAND [ARGS...]" >&2; exit 2; }
code=$(cat <<'PY'
import ctypes
import os
import struct
import sys

SYS_CREATE, SYS_RESTRICT = 444, 446   # x86-64 numbers (arm64 uses the same ones)
CREATE_VERSION = 1
SCOPE_ABSTRACT_UNIX_SOCKET = 1
PR_SET_NO_NEW_PRIVS = 38
CLONE_NEWUSER = 0x10000000

libc = ctypes.CDLL(None, use_errno=True)
userns = sys.argv[1] == "--userns"
if userns:
    del sys.argv[1]
    if len(sys.argv) < 2:
        print("usage: no-abstract-sockets [--userns] COMMAND [ARGS...]", file=sys.stderr)
        sys.exit(2)


def fail(why):
    if os.environ.get("WREN_ALLOW_ABSTRACT_SOCKETS") == "1":
        os.execvp(sys.argv[1], sys.argv[1:])
    print("no-abstract-sockets: " + why, file=sys.stderr)
    print("no-abstract-sockets: (WREN_ALLOW_ABSTRACT_SOCKETS=1 runs it without this protection.)",
          file=sys.stderr)
    sys.exit(1)


abi = libc.syscall(SYS_CREATE, None, 0, CREATE_VERSION)
if abi < 6:
    fail("this kernel can't stop a program reaching abstract sockets (needs Linux 6.12 "
         "with Landlock; version found: %s)." % abi)
# handled_access_fs = 0, handled_access_net = 0: only the scope is set
attr = struct.pack("QQQ", 0, 0, SCOPE_ABSTRACT_UNIX_SOCKET)
fd = libc.syscall(SYS_CREATE, attr, len(attr), 0)
if fd < 0:
    fail("Landlock refused the rule (error %d)." % ctypes.get_errno())
if userns:
    uid, gid = os.getuid(), os.getgid()
    # (mapped, root would keep its rights inside: this is for a sandbox's own user)
    if uid == 0 or gid == 0:
        print("no-abstract-sockets: --userns is for a sandbox's user, not root.", file=sys.stderr)
        sys.exit(1)
    if libc.unshare(CLONE_NEWUSER) != 0:
        fail("couldn't make a user namespace (error %d)." % ctypes.get_errno())
    # (a system can let the namespace be made and then refuse this: Ubuntu's AppArmor does)
    try:
        for name, text in (("setgroups", "deny"), ("uid_map", "%d %d 1" % (uid, uid)), ("gid_map", "%d %d 1" % (gid, gid))):
            with open("/proc/self/" + name, "w") as f:
                f.write(text)
    except OSError as e:
        fail("couldn't make a user namespace (%s)." % e.strerror)
    # XWayland refuses a socket folder that is neither root's nor ours, and root has no number in
    # here: so a folder of ours (memory) goes over it, seen by this program and what it starts alone.
    # (Tried for real with Tor Browser's cage: without it, "/tmp/.X11-unix not owned by root or us".)
    if os.path.isdir("/tmp/.X11-unix"):
        CLONE_NEWNS, MS_REC, MS_PRIVATE = 0x20000, 0x4000, 1 << 18
        if (libc.unshare(CLONE_NEWNS) != 0
                or libc.mount(None, b"/", None, MS_REC | MS_PRIVATE, None) != 0
                or libc.mount(b"tmpfs", b"/tmp/.X11-unix", b"tmpfs", 0, b"mode=1777") != 0):
            fail("couldn't make its own folder for X11 sockets (error %d)." % ctypes.get_errno())
elif libc.prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0:
    fail("couldn't set no_new_privs (error %d)." % ctypes.get_errno())
if libc.syscall(SYS_RESTRICT, fd, 0) != 0:
    fail("Landlock couldn't apply the rule (error %d)." % ctypes.get_errno())
os.close(fd)
try:
    os.execvp(sys.argv[1], sys.argv[1:])
except OSError as e:
    print("no-abstract-sockets: %s: %s" % (sys.argv[1], e.strerror), file=sys.stderr)
    sys.exit(127)
PY
)
exec python3 -I -c "$code" "$@"