#!/usr/bin/env python3 """Clone a FUSE mount from an LXC namespace into the Proxmox host namespace.""" from __future__ import annotations import ctypes import json import os import platform import sys from pathlib import Path from functools import lru_cache @lru_cache(maxsize=1) def _messages() -> dict: # This helper is also installed alone in libexec: importing oci_ui would # break that existing caller. Read the same catalog using stdlib only. base = Path(os.environ.get('PMX_BASE_DIR', '/usr/local/share/proxmenux')) try: language = json.loads((base / 'config.json').read_text(encoding='utf-8')).get('language', 'en') if language == 'en' or not isinstance(language, str) or not language: return {} data = json.loads((base / 'lang' / (language + '.json')).read_text(encoding='utf-8')) return data if isinstance(data, dict) else {} except (OSError, ValueError, AttributeError): return {} def translate(text: str) -> str: value = _messages().get(text) return value if isinstance(value, str) and value else text AT_FDCWD = -100 AT_EMPTY_PATH = 0x1000 AT_RECURSIVE = 0x8000 CLONE_NEWNS = 0x00020000 MOVE_MOUNT_F_EMPTY_PATH = 0x00000004 MOUNT_ATTR_RDONLY = 0x00000001 OPEN_TREE_CLONE = 1 SYSCALLS = { "x86_64": (428, 429, 442), "amd64": (428, 429, 442), "aarch64": (428, 429, 442), "arm64": (428, 429, 442), } class MountAttr(ctypes.Structure): _fields_ = [ ("attr_set", ctypes.c_uint64), ("attr_clr", ctypes.c_uint64), ("propagation", ctypes.c_uint64), ("userns_fd", ctypes.c_uint64), ] def fail(step: str) -> None: error = ctypes.get_errno() raise OSError(error, f"{step}: {os.strerror(error)}") def main() -> int: if len(sys.argv) != 5 or sys.argv[4] not in {"rw", "ro"}: print(f"{translate('Usage:')} {sys.argv[0]} PID SOURCE TARGET rw|ro", file=sys.stderr) return 2 machine = platform.machine().lower() if machine not in SYSCALLS: print(f"{translate('Unsupported host architecture:')} {machine}", file=sys.stderr) return 2 open_tree_nr, move_mount_nr, mount_setattr_nr = SYSCALLS[machine] pid, source, target, mode = sys.argv[1:] # The host catalog is no longer reachable after chroot into the container. _messages() libc = ctypes.CDLL(None, use_errno=True) libc.syscall.restype = ctypes.c_long libc.setns.argtypes = (ctypes.c_int, ctypes.c_int) libc.setns.restype = ctypes.c_int host_ns = os.open("/proc/self/ns/mnt", os.O_RDONLY | os.O_CLOEXEC) host_root = os.open("/", os.O_PATH | os.O_DIRECTORY | os.O_CLOEXEC) ct_ns = os.open(f"/proc/{pid}/ns/mnt", os.O_RDONLY | os.O_CLOEXEC) ct_root = os.open(f"/proc/{pid}/root", os.O_PATH | os.O_DIRECTORY | os.O_CLOEXEC) try: if libc.setns(ct_ns, CLONE_NEWNS) != 0: fail(translate("enter container namespace")) os.fchdir(ct_root) os.chroot(".") os.chdir("/") tree = libc.syscall( open_tree_nr, AT_FDCWD, os.fsencode(source), OPEN_TREE_CLONE | os.O_CLOEXEC, ) if tree < 0: fail(translate("clone source mount tree")) try: if mode == "ro": attributes = MountAttr(attr_set=MOUNT_ATTR_RDONLY) result = libc.syscall( mount_setattr_nr, tree, ctypes.c_char_p(b""), AT_EMPTY_PATH | AT_RECURSIVE, ctypes.byref(attributes), ctypes.sizeof(attributes), ) if result != 0: fail(translate("make cloned mount tree read-only")) if libc.setns(host_ns, CLONE_NEWNS) != 0: fail(translate("return to host namespace")) os.fchdir(host_root) os.chroot(".") os.chdir("/") result = libc.syscall( move_mount_nr, tree, ctypes.c_char_p(b""), AT_FDCWD, os.fsencode(target), MOVE_MOUNT_F_EMPTY_PATH, ) if result != 0: fail(translate("publish mount tree")) finally: os.close(tree) finally: for descriptor in (ct_root, ct_ns, host_root, host_ns): os.close(descriptor) return 0 if __name__ == "__main__": try: raise SystemExit(main()) except OSError as exc: print(exc, file=sys.stderr) raise SystemExit(1)