From be5e18cf998eb5b84f89aac00dc9ca13e02d801a Mon Sep 17 00:00:00 2001 From: Casey Connolly Date: Sat, 3 May 2025 20:55:14 +0200 Subject: [PATCH] unshare binfmt Rework how we handle binfmt_misc so it will work inside a user namespace. * Use a custom mountpoint (only accessible inside the mount namespace), this is the crux of the change, allowing us to mount it as non-root and avoid messing with any host configs too! * No longer explicitly modprobe binfmt_misc, any modern system should probe it automatically when we try to mount it... I think so anyways heh Signed-off-by: Casey Connolly --- pmb/chroot/binfmt.py | 57 +++++++++++++++++++++++++++++----------- pmb/config/__init__.py | 3 +++ pmb/config/init.py | 1 + pmb/helpers/other.py | 25 ------------------ pmb/init/sandbox.py | 12 +++++++++ pmb/parse/__init__.py | 1 - pmb/parse/binfmt_info.py | 33 ----------------------- pmbootstrap.py | 4 ++- 8 files changed, 60 insertions(+), 76 deletions(-) delete mode 100644 pmb/parse/binfmt_info.py diff --git a/pmb/chroot/binfmt.py b/pmb/chroot/binfmt.py index e5a76adf..025d6925 100644 --- a/pmb/chroot/binfmt.py +++ b/pmb/chroot/binfmt.py @@ -1,18 +1,41 @@ # Copyright 2023 Oliver Smith # SPDX-License-Identifier: GPL-3.0-or-later + import os from pmb.core.arch import Arch from pmb.core.chroot import Chroot from pmb.helpers import logging - -import pmb.helpers.run -import pmb.helpers.other -import pmb.parse -import pmb.chroot.apk +import pmb.config def is_registered(arch_qemu: str | Arch) -> bool: - return os.path.exists(f"/proc/sys/fs/binfmt_misc/qemu-{arch_qemu}") + return os.path.exists(f"{pmb.config.binfmt_misc}/qemu-{arch_qemu}") + + +# FIXME: Maybe this should use Arch instead of str. +def parse_binfmt_info(arch_qemu: str) -> dict[str, str]: + # Parse the info file + full = {} + info = pmb.config.pmb_src / "pmb/data/qemu-user-binfmt.txt" + logging.verbose(f"parsing: {info}") + with open(info) as handle: + for line in handle: + if line.startswith("#") or "=" not in line: + continue + split = line.split("=") + key = split[0].strip() + value = split[1] + full[key] = value[1:-2] + + ret = {} + logging.verbose("filtering by architecture: " + arch_qemu) + for type in ["mask", "magic"]: + key = arch_qemu + "_" + type + if key not in full: + raise RuntimeError(f"Could not find key {key} in binfmt info file: {info}") + ret[type] = full[key] + logging.verbose("=> " + str(ret)) + return ret def register(arch: Arch) -> None: @@ -28,16 +51,11 @@ def register(arch: Arch) -> None: pmb.chroot.init(chroot) pmb.chroot.apk.install(["qemu-" + arch_qemu], chroot) - if is_registered(arch_qemu): - return - pmb.helpers.other.check_binfmt_misc() - - # Don't continue if the actions from check_binfmt_misc caused the OS to - # automatically register the target arch + # Check if we're already registered if is_registered(arch_qemu): return - info = pmb.parse.binfmt_info(arch_qemu) + info = parse_binfmt_info(arch_qemu) # Build registration string # https://en.wikipedia.org/wiki/Binfmt_misc @@ -47,20 +65,27 @@ def register(arch: Arch) -> None: offset = "" magic = info["magic"] mask = info["mask"] + + # FIXME: this relies on a hack where we bind-mount the qemu interpreter into the foreign + # chroot. This really shouldn't be needed, instead we should unshare pmbootstrap into + # an Alpine chroot that would have the interpreter installed, then pass the 'F' flag which + # allows the interpreter to always be run even when we're later in a chroot. interpreter = "/usr/bin/qemu-" + arch_qemu + "-static" flags = "C" code = ":".join(["", name, type, offset, magic, mask, interpreter, flags]) # Register in binfmt_misc logging.info("Register qemu binfmt (" + arch_qemu + ")") - register = "/proc/sys/fs/binfmt_misc/register" + register = f"{pmb.config.binfmt_misc}/register" pmb.helpers.run.root(["sh", "-c", 'echo "' + code + '" > ' + register]) + logging.warning("WARNING: FIXME: binfmt borked because no perms!") def unregister(arch: Arch) -> None: arch_qemu = arch.qemu() - binfmt_file = "/proc/sys/fs/binfmt_misc/qemu-" + arch_qemu + binfmt_file = f"{pmb.config.binfmt_misc}/qemu-" + arch_qemu if not os.path.exists(binfmt_file): return logging.info("Unregister qemu binfmt (" + arch_qemu + ")") - pmb.helpers.run.root(["sh", "-c", "echo -1 > " + binfmt_file]) + # pmb.helpers.run.root(["sh", "-c", "echo -1 > " + binfmt_file]) + logging.warning("WARNING: FIXME: binfmt borked because no perms!") diff --git a/pmb/config/__init__.py b/pmb/config/__init__.py index bff9452a..b521850e 100644 --- a/pmb/config/__init__.py +++ b/pmb/config/__init__.py @@ -23,6 +23,9 @@ from . import workdir as workdir pmb_src: Path = Path(Path(__file__) / "../../..").resolve() apk_keys_path: Path = pmb_src / "pmb/data/keys" +# In the mount namespace this is where we mount our own binfmt_misc dir +binfmt_misc = "/tmp/pmb_binfmt_misc" + # apk-tools minimum version # https://pkgs.alpinelinux.org/packages?name=apk-tools&branch=edge # Update this frequently to prevent a MITM attack with an outdated version diff --git a/pmb/config/init.py b/pmb/config/init.py index c5f97a5e..6910854c 100644 --- a/pmb/config/init.py +++ b/pmb/config/init.py @@ -1,5 +1,6 @@ # Copyright 2023 Oliver Smith # SPDX-License-Identifier: GPL-3.0-or-later +from pmb.core.arch import Arch from pmb.core.context import get_context from pmb.core.chroot import Chroot from pmb.core.config import SystemdConfig diff --git a/pmb/helpers/other.py b/pmb/helpers/other.py index ee5f2a41..c22de7e7 100644 --- a/pmb/helpers/other.py +++ b/pmb/helpers/other.py @@ -47,31 +47,6 @@ def check_grsec() -> None: ) -def check_binfmt_misc() -> None: - """Check if the 'binfmt_misc' module is loaded. - - This is done by checking, if /proc/sys/fs/binfmt_misc/ exists. - If it exists, then do nothing. - Otherwise, load the module and mount binfmt_misc. - If that fails as well, raise an exception pointing the user to the wiki. - """ - path = "/proc/sys/fs/binfmt_misc/status" - if os.path.exists(path): - return - - # check=False: this might be built-in instead of being a module - pmb.helpers.run.root(["modprobe", "binfmt_misc"], check=False) - - # check=False: we check it below and print a more helpful message on error - pmb.helpers.run.root( - ["mount", "-t", "binfmt_misc", "none", "/proc/sys/fs/binfmt_misc"], check=False - ) - - if not os.path.exists(path): - link = "https://postmarketos.org/binfmt_misc" - raise RuntimeError(f"Failed to set up binfmt_misc, see: {link}") - - def migrate_success(work: Path, version: int) -> None: logging.info("Migration to version " + str(version) + " done") with open(work / "version", "w") as handle: diff --git a/pmb/init/sandbox.py b/pmb/init/sandbox.py index d24fa5d2..72bf7165 100644 --- a/pmb/init/sandbox.py +++ b/pmb/init/sandbox.py @@ -1,4 +1,6 @@ # SPDX-License-Identifier: LGPL-2.1-or-later +# FIXME: this file is wayyy off lol +# ruff: noqa """ This is a standalone implementation of sandboxing which is used by mkosi. Note that this is @@ -30,6 +32,7 @@ CLONE_NEWNS = 0x00020000 CLONE_NEWUSER = 0x10000000 EBADF = 9 UNSHARE_EPERM_MSGEPERM = 1 +EPERM = 1 ENOENT = 2 ENOSYS = 38 F_DUPFD = 0 @@ -671,6 +674,15 @@ class ProcOperation(FSOperation): mount_rbind(joinpath(oldroot, "proc"), dst) +class BinfmtOperation(FSOperation): + def execute(self, oldroot: str, newroot: str) -> None: + dst = chase(newroot, self.dst) + with umask(~0o755): + os.makedirs(dst, exist_ok=True) + + mount("binfmt_misc", dst, "binfmt_misc", 0, "") + + class DevOperation(FSOperation): def __init__(self, ttyname: str, dst: str) -> None: self.ttyname = ttyname diff --git a/pmb/parse/__init__.py b/pmb/parse/__init__.py index 87dee8ab..dd4817d7 100644 --- a/pmb/parse/__init__.py +++ b/pmb/parse/__init__.py @@ -8,7 +8,6 @@ from pmb.parse.arguments import ( ) from pmb.parse._apkbuild import apkbuild as apkbuild from pmb.parse._apkbuild import function_body as function_body -from pmb.parse.binfmt_info import binfmt_info as binfmt_info from pmb.parse.deviceinfo import deviceinfo as deviceinfo from pmb.parse.kconfig import check as check from pmb.parse.bootimg import bootimg as bootimg diff --git a/pmb/parse/binfmt_info.py b/pmb/parse/binfmt_info.py deleted file mode 100644 index 96c866e1..00000000 --- a/pmb/parse/binfmt_info.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2023 Oliver Smith -# SPDX-License-Identifier: GPL-3.0-or-later -from pmb.helpers import logging -import pmb.config - -# Get magic and mask from binfmt info file -# Return: {magic: ..., mask: ...} - - -# FIXME: Maybe this should use Arch instead of str. -def binfmt_info(arch_qemu: str) -> dict[str, str]: - # Parse the info file - full = {} - info = pmb.config.pmb_src / "pmb/data/qemu-user-binfmt.txt" - logging.verbose(f"parsing: {info}") - with open(info) as handle: - for line in handle: - if line.startswith("#") or "=" not in line: - continue - split = line.split("=") - key = split[0].strip() - value = split[1] - full[key] = value[1:-2] - - ret = {} - logging.verbose("filtering by architecture: " + arch_qemu) - for type in ["mask", "magic"]: - key = arch_qemu + "_" + type - if key not in full: - raise RuntimeError(f"Could not find key {key} in binfmt info file: {info}") - ret[type] = full[key] - logging.verbose("=> " + str(ret)) - return ret diff --git a/pmbootstrap.py b/pmbootstrap.py index b231c478..3ef50180 100755 --- a/pmbootstrap.py +++ b/pmbootstrap.py @@ -34,7 +34,9 @@ fsops = [ readonly=False, required=True, relative=False, - ) + ), + # Mount binfmt_misc at /tmp/pmb_binfmt_misc + sandbox.BinfmtOperation(pmb.config.binfmt_misc), ] sandbox.setup_mounts(fsops)