more things
This commit is contained in:
parent
1cd4394841
commit
6e60709283
15 changed files with 1403 additions and 294 deletions
151
tools/run_vm.py
151
tools/run_vm.py
|
|
@ -11,12 +11,19 @@ examples:
|
|||
python3 tools/run_vm.py --fetch-busybox --nographic
|
||||
python3 tools/run_vm.py --kernel /boot/vmlinuz-$(uname -r) --gdb
|
||||
python3 tools/run_vm.py --config my-init.rc
|
||||
python3 tools/run_vm.py --selinux # boot with the host SELinux policy (permissive)
|
||||
|
||||
inside the guest: log in as `root` (passwordless by default, or use
|
||||
--root-password), then `kill -TERM 1` -> reboot path, `kill -INT 1` ->
|
||||
--root-password). `bctl status` / `bctl shutdown poweroff` drive the
|
||||
init over its control socket; `kill -TERM 1` -> reboot path, `kill -INT 1` ->
|
||||
poweroff path.
|
||||
bajia is built statically by default: a dynamic binary cannot exec inside the
|
||||
initramfs (no libc there). Use --no-static only if you ship the libs too.
|
||||
--selinux flips bajia to a dynamic build (there is no static libselinux on
|
||||
Fedora), bundles libselinux/libpcre2/glibc + the loader into the initramfs,
|
||||
and copies the host's /etc/selinux/<type> policy (loaded permissively). This
|
||||
is the first stage of bootstrapping a real policy: boot, read the `avc:
|
||||
denied` lines, refine, then flip to enforcing.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -60,11 +67,11 @@ on shutdown
|
|||
|
||||
service console-serial /bin/getty -L ttyS0 115200 vt100
|
||||
console
|
||||
respawn always
|
||||
respawn = always
|
||||
|
||||
service console-tty1 /bin/getty -L 38400 tty1 vt100
|
||||
console
|
||||
respawn always
|
||||
respawn = always
|
||||
"""
|
||||
|
||||
# source tarballs of busybox (github.com/mirror/busybox); a pinned tag is
|
||||
|
|
@ -77,18 +84,19 @@ BUSYBOX_URLS = [
|
|||
BUSYBOX_APPLETS = ["sh", "getty", "mount", "sync", "ls", "cat", "kill", "ps",
|
||||
"poweroff", "reboot", "mkdir", "mknod", "login"]
|
||||
|
||||
|
||||
def run(cmd, **kw) -> subprocess.CompletedProcess:
|
||||
print("$", " ".join(str(c) for c in cmd))
|
||||
return subprocess.run(cmd, **kw)
|
||||
|
||||
|
||||
def build_bajia(static: bool) -> Path:
|
||||
def build_bajia(static: bool, selinux: bool = False) -> Path:
|
||||
env = dict(os.environ)
|
||||
cfg = ["python3", "configure.py"]
|
||||
if selinux:
|
||||
cfg.append("--selinux")
|
||||
if static:
|
||||
print("building bajia (statically linked)...")
|
||||
env["CXX"] = env.get("CXX", "g++") + " -static"
|
||||
r = run(["python3", "configure.py"], env=env)
|
||||
r = run(cfg, env=env)
|
||||
if r.returncode != 0:
|
||||
sys.exit("configure.py failed")
|
||||
r = run(["ninja", "-C", str(BUILD)])
|
||||
|
|
@ -96,6 +104,64 @@ def build_bajia(static: bool) -> Path:
|
|||
sys.exit("ninja build failed")
|
||||
return BAJIA
|
||||
|
||||
SELINUX_CONFIG = Path("/etc/selinux/config")
|
||||
|
||||
def host_selinux_type() -> str:
|
||||
if not SELINUX_CONFIG.is_file():
|
||||
return "targeted"
|
||||
for line in SELINUX_CONFIG.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("SELINUXTYPE="):
|
||||
return line.split("=", 1)[1].strip().strip('"')
|
||||
return "targeted"
|
||||
|
||||
def selinux_policy_files() -> list[tuple[Path, str]]:
|
||||
"""Return (host_path, initramfs_relative_path) pairs for the policy payload."""
|
||||
typ = host_selinux_type()
|
||||
base = Path("/etc/selinux") / typ
|
||||
pols = sorted((base / "policy").glob("policy.*"))
|
||||
if not pols:
|
||||
sys.exit(f"--selinux: no policy under {base / 'policy'}/ "
|
||||
"(install selinux-policy-targeted, a Fedora SELinux host is assumed)")
|
||||
# libselinux 3.x looks for the restorecon table at contexts/files/file_contexts
|
||||
# (older versions used contexts/file_contexts); bundle whichever exists.
|
||||
fc = next((p for p in (base / "contexts" / "files" / "file_contexts",
|
||||
base / "contexts" / "file_contexts") if p.is_file()), None)
|
||||
if not fc:
|
||||
sys.exit(f"--selinux: missing file_contexts under {base / 'contexts'}/")
|
||||
fc_rel = "etc/selinux/" + typ + "/" + str(fc.relative_to(base))
|
||||
prefix = f"etc/selinux/{typ}/"
|
||||
return [(pols[-1], prefix + f"policy/{pols[-1].name}"),
|
||||
(fc, fc_rel)]
|
||||
|
||||
def bundle_dynamic_libs(init: Path, root: Path) -> None:
|
||||
"""Copy the dynamic loader + resolved .so deps into the initramfs,
|
||||
mirroring their absolute paths so the interpreter finds them."""
|
||||
out = run(["ldd", str(init)], capture_output=True, text=True)
|
||||
if out.returncode != 0:
|
||||
sys.exit("ldd failed on " + str(init))
|
||||
libs: list[str] = []
|
||||
for line in out.stdout.splitlines():
|
||||
line = line.strip()
|
||||
if "=>" in line:
|
||||
path = line.split("=>", 1)[1].strip().split(" ", 1)[0].strip()
|
||||
else: # "linux-vdso" or the loader line "/lib64/ld-linux-x86-64.so.2 (0x...)"
|
||||
path = line.split(" ", 1)[0].strip()
|
||||
if path.startswith("/") and Path(path).is_file():
|
||||
libs.append(path)
|
||||
seen: set[str] = set()
|
||||
for lib in libs: # preserve first-seen order
|
||||
if lib in seen:
|
||||
continue
|
||||
seen.add(lib)
|
||||
dst = root / lib.lstrip("/")
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy(lib, dst)
|
||||
dst.chmod(0o755)
|
||||
print("bundled dynamic libs:", ", ".join(seen))
|
||||
if not seen:
|
||||
sys.exit("ldd reported no libraries - unexpected for a dynamic binary")
|
||||
|
||||
def find_kernel() -> Path | None:
|
||||
p = Path("/boot/vmlinuz-" + os.uname().release)
|
||||
if p.is_file():
|
||||
|
|
@ -194,8 +260,20 @@ def root_passwd_line(password: str | None) -> str:
|
|||
field = crypt_password(password) if password else ""
|
||||
return f"root:{field}:0:0:root:/:/bin/sh\n"
|
||||
|
||||
# appended to the test init.rc; started on boot, prints the exec context of a
|
||||
# seclabel'd service and of init itself, then exits.
|
||||
SELINUX_RC_PROBE = """
|
||||
service selinux-probe /bin/sh -c "echo probe-ctx=$(cat /proc/self/attr/current) init-ctx=$(cat /proc/1/attr/current)"
|
||||
console
|
||||
seclabel = system_u:system_r:init_t:s0
|
||||
respawn = never
|
||||
|
||||
on boot
|
||||
start selinux-probe
|
||||
"""
|
||||
|
||||
def build_initramfs(init: Path, busybox: Path, rc_text: str, root_password: str | None,
|
||||
keep: bool) -> Path:
|
||||
selinux: bool, keep: bool) -> Path:
|
||||
if not shutil.which("cpio"):
|
||||
sys.exit("cpio not found (install cpio)")
|
||||
root = Path(tempfile.mkdtemp(prefix="bajia-root-"))
|
||||
|
|
@ -204,19 +282,52 @@ def build_initramfs(init: Path, busybox: Path, rc_text: str, root_password: str
|
|||
"dev", "proc", "sys", "run", "tmp"):
|
||||
(root / sub).mkdir(parents=True)
|
||||
|
||||
if selinux:
|
||||
for src, rel in selinux_policy_files():
|
||||
dst = root / rel
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy(src, dst)
|
||||
selcfg = root / "etc" / "selinux" / "config"
|
||||
selcfg.write_text(f"SELINUX=permissive\nSELINUXTYPE={host_selinux_type()}\n")
|
||||
bundle_dynamic_libs(init, root)
|
||||
elif not is_static(init):
|
||||
print("warning: bajia is dynamically linked; /init will fail to exec "
|
||||
"inside the initramfs (error -2). Rebuild with --no-static "
|
||||
"unset (static is the default) or drop --no-build.")
|
||||
|
||||
if selinux:
|
||||
rc_text = rc_text + SELINUX_RC_PROBE
|
||||
|
||||
shutil.copy(init, root / "init")
|
||||
(root / "init").chmod(0o755)
|
||||
|
||||
ctl = BUILD / "bctl"
|
||||
if ctl.is_file():
|
||||
shutil.copy(ctl, root / "bin" / "bctl")
|
||||
(root / "bin" / "bctl").chmod(0o755)
|
||||
|
||||
shutil.copy(busybox, root / "bin" / "busybox")
|
||||
(root / "bin" / "busybox").chmod(0o755)
|
||||
for applet in BUSYBOX_APPLETS:
|
||||
(root / "bin" / applet).symlink_to("busybox")
|
||||
|
||||
(root / "etc" / "bajia" / "init.rc").write_text(rc_text)
|
||||
(root / "etc" / "passwd").write_text(root_passwd_line(root_password))
|
||||
(root / "etc" / "passwd").write_text(
|
||||
root_passwd_line(root_password) +
|
||||
"nobody:x:65534:65534:nobody:/:/bin/sh\n")
|
||||
(root / "etc" / "group").write_text(
|
||||
"root:x:0:\n"
|
||||
"nobody:x:65534:\n"
|
||||
"daemon:x:1:\n")
|
||||
|
||||
# The staging tree is owned by the host user and mkdtemp makes the
|
||||
# top dir 0700; GNU cpio preserves both, so without this the guest's
|
||||
# "/" would be mode 0700 owned by uid 1000 -- fine for root services,
|
||||
# but dropped-privilege services couldn't traverse it. Stamp owner
|
||||
# root:root (no host chown needed) and make the root traversable.
|
||||
p = run(["bash", "-c",
|
||||
"cd \"$1\" && find . -print0 | cpio --null -o -H newc",
|
||||
"cd \"$1\" && chmod 0755 . && "
|
||||
"find . -print0 | cpio --null -o -H newc --owner=0:0",
|
||||
"bajia-initramfs", str(root)], stdout=subprocess.PIPE)
|
||||
if p.returncode != 0:
|
||||
sys.exit("cpio packing failed")
|
||||
|
|
@ -235,6 +346,9 @@ def qemu_command(kernel: Path, initrd: Path, args: argparse.Namespace) -> list[s
|
|||
display = args.display
|
||||
if display is None:
|
||||
display = "gtk" if os.environ.get("DISPLAY") else "none"
|
||||
append = (f"console=tty1 console=ttyS0 rdinit=/init loglevel={args.loglevel}")
|
||||
if args.selinux:
|
||||
append += " selinux=1 enforcing=0"
|
||||
cmd = [
|
||||
qemu,
|
||||
"-M", args.machine,
|
||||
|
|
@ -242,13 +356,15 @@ def qemu_command(kernel: Path, initrd: Path, args: argparse.Namespace) -> list[s
|
|||
"-smp", str(args.smp),
|
||||
"-kernel", str(kernel),
|
||||
"-initrd", str(initrd),
|
||||
"-append",
|
||||
f"console=tty1 console=ttyS0 rdinit=/init loglevel={args.loglevel}",
|
||||
"-append", append,
|
||||
"-display", display,
|
||||
"-serial", "stdio",
|
||||
]
|
||||
if args.nographic:
|
||||
cmd[cmd.index("-display") + 1] = "none"
|
||||
if args.serial_log:
|
||||
cmd[cmd.index("-display") + 1] = "none"
|
||||
cmd[cmd.index("-serial") + 1] = f"file:{args.serial_log}"
|
||||
if args.gdb or args.wait_gdb:
|
||||
cmd += ["-gdb", "tcp::1234", "-S"] if args.wait_gdb else ["-s"]
|
||||
cmd += ["-no-reboot", "-no-shutdown"]
|
||||
|
|
@ -287,12 +403,17 @@ def main() -> int:
|
|||
help="pause the machine until a gdb client attaches")
|
||||
ap.add_argument("--keep-initramfs", action="store_true",
|
||||
help="don't delete the initramfs staging tree")
|
||||
ap.add_argument("--selinux", action="store_true",
|
||||
help="bundle the host SELinux policy + libs, boot permissive")
|
||||
ap.add_argument("--serial-log", type=Path,
|
||||
help="write the serial console to this file (forces -display none)")
|
||||
args = ap.parse_args()
|
||||
|
||||
init = BAJIA if args.no_build else build_bajia(not args.no_static)
|
||||
static = not args.no_static and not args.selinux
|
||||
init = BAJIA if args.no_build else build_bajia(static, args.selinux)
|
||||
if not init.is_file():
|
||||
sys.exit(f"bajia not built at {init} (drop --no-build)")
|
||||
if not is_static(init):
|
||||
if not static and not args.selinux and not is_static(init):
|
||||
print("warning: bajia is dynamically linked; /init will fail to exec "
|
||||
"inside the initramfs (error -2). Rebuild with --no-static "
|
||||
"unset (static is the default) or drop --no-build.")
|
||||
|
|
@ -315,7 +436,7 @@ def main() -> int:
|
|||
|
||||
rc_text = args.config.read_text() if args.config else DEFAULT_RC
|
||||
initrd = build_initramfs(init, busybox, rc_text, args.root_password,
|
||||
keep=args.keep_initramfs)
|
||||
selinux=args.selinux, keep=args.keep_initramfs)
|
||||
print("initramfs:", initrd, f"({initrd.stat().st_size / 1024:.0f} KB)")
|
||||
|
||||
cmd = qemu_command(kernel, initrd, args)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue