1
0
Fork 1
mirror of https://gitlab.postmarketos.org/postmarketOS/pmbootstrap.git synced 2025-07-23 04:25:10 +03:00

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 <kcxt@postmarketos.org>
This commit is contained in:
Casey Connolly 2025-05-03 20:55:14 +02:00
parent c997ce83c2
commit be5e18cf99
No known key found for this signature in database
GPG key ID: 0583312B195F64B6
8 changed files with 60 additions and 76 deletions

View file

@ -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!")

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)