add logging and other things

This commit is contained in:
Hedy88 2026-08-28 23:24:19 +01:00
commit 067cadbe0f
No known key found for this signature in database
10 changed files with 870 additions and 98 deletions

View file

@ -272,83 +272,168 @@ on boot
start selinux-probe
"""
def build_initramfs(init: Path, busybox: Path, rc_text: str, root_password: str | None,
selinux: bool, keep: bool,
bundles: list[tuple[str, Path]] | None = None) -> Path:
if not shutil.which("cpio"):
sys.exit("cpio not found (install cpio)")
root = Path(tempfile.mkdtemp(prefix="bajia-root-"))
try:
for sub in ("etc/bajia", "bin", "sbin", "usr/sbin", "usr/bin",
"dev", "proc", "sys", "run", "tmp"):
(root / sub).mkdir(parents=True)
# subdirectories laid out in every staging root tree (initramfs and disk root).
COMMON_SUBDIRS = ("etc/bajia", "bin", "sbin", "usr/sbin", "usr/bin",
"dev", "proc", "sys", "run", "tmp", "mnt")
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.")
def stage_root_tree(root: Path, init: Path, init_rel: str, busybox: Path,
rc_text: str, rc_rel: str, root_password: str | None,
selinux: bool, bundles: list[tuple[str, Path]]) -> None:
"""Lay out the common bajia + busybox tree into `root`.
if selinux:
rc_text = rc_text + SELINUX_RC_PROBE
`init_rel` is where bajia lands ('init' for the stage-1 initramfs,
'sbin/init' for the stage-2 root disk); `rc_rel` is where init.rc lands.
The core payload is identical for both stages."""
for sub in COMMON_SUBDIRS:
(root / sub).mkdir(parents=True)
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) +
"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")
for rel, src in (bundles or []):
dst = root / rel.lstrip("/")
if selinux:
for src, rel in selinux_policy_files():
dst = root / rel
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(src, dst)
dst.chmod(0o644)
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.")
# 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\" && 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")
initrd = Path(tempfile.gettempdir()) / "bajia-initrd.cpio.gz"
initrd.write_bytes(gzip.compress(p.stdout))
if selinux:
rc_text = rc_text + SELINUX_RC_PROBE
if keep:
print("initramfs root tree kept at:", root)
return initrd
dst = root / init_rel
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(init, dst)
dst.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")
rcdst = root / rc_rel
rcdst.parent.mkdir(parents=True, exist_ok=True)
rcdst.write_text(rc_text)
(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")
for rel, src in (bundles or []):
dst = root / rel.lstrip("/")
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(src, dst)
dst.chmod(0o644)
# traversable by dropped-privilege services; the packers stamp ownership.
root.chmod(0o755)
def pack_cpio(root: Path, keep: bool) -> Path:
if not shutil.which("cpio"):
sys.exit("cpio not found (install cpio)")
# The staging tree is owned by the host user; --owner=0:0 and a traversable
# root make every path root-owned inside the guest.
p = run(["bash", "-c",
"cd \"$1\" && 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")
initrd = Path(tempfile.gettempdir()) / "bajia-initrd.cpio.gz"
initrd.write_bytes(gzip.compress(p.stdout))
if keep:
print("initramfs root tree kept at:", root)
return initrd
def pack_ext4(root: Path, keep: bool, size_mb: int = 128) -> Path:
"""Pack `root` into a writable ext4 disk image via `mkfs.ext4 -d`.
No loop mount needed, so it works unprivileged. The image is the second
stage's real root filesystem, attached to the guest as a virtio-blk disk."""
if not shutil.which("mkfs.ext4"):
sys.exit("mkfs.ext4 not found (install e2fsprogs)")
img = Path(tempfile.gettempdir()) / "bajia-stage2.ext4"
if img.is_file():
img.unlink()
# mkfs.ext4 -d does not create the image file; pre-size a sparse file.
r = run(["truncate", "-s", f"{size_mb}M", str(img)])
if r.returncode != 0:
sys.exit("truncate failed")
r = run(["mkfs.ext4", "-q", "-d", str(root), "-F", str(img)])
if r.returncode != 0:
sys.exit("mkfs.ext4 failed")
if keep:
print("stage2 root tree kept at:", root)
return img
def build_initramfs(init: Path, busybox: Path, rc_text: str,
root_password: str | None, selinux: bool, keep: bool,
bundles: list[tuple[str, Path]] | None = None) -> Path:
root = Path(tempfile.mkdtemp(prefix="bajia-root-"))
try:
stage_root_tree(root, init, "init", busybox, rc_text,
"etc/bajia/init.rc", root_password, selinux, bundles)
return pack_cpio(root, keep)
finally:
if not keep:
shutil.rmtree(root, ignore_errors=True)
def qemu_command(kernel: Path, initrd: Path, args: argparse.Namespace) -> list[str]:
# first-stage config for --two-stage: bring up the basics, mount the real root
# disk, and switch_root onto it. `root_dev` is the virtio-blk target (/dev/vda).
TWO_STAGE_FIRST_RC = """\
# generated by tools/run_vm.py --two-stage - first stage (initramfs).
on early-init
mount proc /proc proc
mount sysfs /sys sysfs
mount devtmpfs /dev devtmpfs
on init
mkdir /mnt/root 0755
mount {dev} /mnt/root {fstype}
on boot
switch_root /mnt/root /sbin/init /etc/bajia/init.rc
"""
def build_two_stage(init: Path, busybox: Path, second_rc: str,
root_password: str | None, selinux: bool, keep: bool,
bundles: list[tuple[str, Path]],
root_dev: str = "/dev/vda",
root_fstype: str = "ext4",
root_size_mb: int = 128) -> tuple[Path, Path]:
"""Return (stage1_initrd, stage2_root_img) for a classic two-stage boot.
stage1 is a minimal initramfs running bajia as /init with a generated
first-stage config that mounts the real root and switch_roots onto it.
stage2 is a writable ext4 disk image (the "real root"), bundled with its
own copy of bajia at /sbin/init plus the full init.rc and services."""
first_rc = TWO_STAGE_FIRST_RC.format(dev=root_dev, fstype=root_fstype)
root1 = Path(tempfile.mkdtemp(prefix="bajia-stage1-"))
root2 = Path(tempfile.mkdtemp(prefix="bajia-stage2-"))
try:
stage_root_tree(root1, init, "init", busybox, first_rc,
"etc/bajia/init.rc", root_password, selinux, [])
initrd = pack_cpio(root1, keep)
stage_root_tree(root2, init, "sbin/init", busybox, second_rc,
"etc/bajia/init.rc", root_password, selinux, bundles)
img = pack_ext4(root2, keep, size_mb=root_size_mb)
return initrd, img
finally:
if not keep:
shutil.rmtree(root1, ignore_errors=True)
shutil.rmtree(root2, ignore_errors=True)
def qemu_command(kernel: Path, initrd: Path, args: argparse.Namespace,
root_img: Path | None = None) -> list[str]:
qemu = args.qemu or shutil.which("qemu-system-x86_64") or "qemu-system-x86_64"
display = args.display
if display is None:
@ -367,6 +452,9 @@ def qemu_command(kernel: Path, initrd: Path, args: argparse.Namespace) -> list[s
"-display", display,
"-serial", "stdio",
]
if root_img is not None:
# second-stage root disk; virtio-blk (built into modern kernels) -> /dev/vda
cmd += ["-drive", f"file={root_img},format=raw,if=virtio"]
if args.nographic:
cmd[cmd.index("-display") + 1] = "none"
if args.serial_log:
@ -386,7 +474,22 @@ def main() -> int:
help="download busybox source (github.com/mirror/busybox), "
"build it static, cache in ~/.cache/bajia")
ap.add_argument("--config", type=Path,
help="use this init.rc instead of the bundled test config")
help="use this init.rc instead of the bundled test config "
"(with --two-stage this is the *second stage* config)")
ap.add_argument("--two-stage", action="store_true",
help="boot a classic two-stage initramfs: a minimal stage-1 "
"initramfs runs bajia as /init, mounts a real root disk "
"and switch_roots onto it; stage-2 is a writable ext4 "
"root running bajia from /sbin/init")
ap.add_argument("--root-dev", default="/dev/vda",
help="--two-stage: block device for the real root "
"(default /dev/vda)")
ap.add_argument("--root-fstype", default="ext4",
help="--two-stage: filesystem type of the real root "
"(default ext4)")
ap.add_argument("--root-size", type=int, default=128,
help="--two-stage: stage-2 root image size in MiB "
"(default 128)")
ap.add_argument("--bundle", action="append", default=[],
metavar="REL=HOSTPATH",
help="copy HOSTPATH into the initramfs at absolute REL "
@ -458,12 +561,27 @@ def main() -> int:
if ".." in [c for c in Path(rel).parts]:
sys.exit(f"--bundle: REL must not contain '..': {rel}")
bundles.append((rel, src))
initrd = build_initramfs(init, busybox, rc_text, args.root_password,
selinux=args.selinux, keep=args.keep_initramfs,
bundles=bundles)
print("initramfs:", initrd, f"({initrd.stat().st_size / 1024:.0f} KB)")
cmd = qemu_command(kernel, initrd, args)
if args.two_stage:
if args.selinux:
print("note: SELinux is bundled into both stages' roots")
initrd, root_img = build_two_stage(
init, busybox, rc_text, args.root_password,
selinux=args.selinux, keep=args.keep_initramfs, bundles=bundles,
root_dev=args.root_dev, root_fstype=args.root_fstype,
root_size_mb=args.root_size)
print("stage1 initramfs:", initrd,
f"({initrd.stat().st_size / 1024:.0f} KB)")
print("stage2 root disk:", root_img,
f"({root_img.stat().st_size / 1024:.0f} KB)")
else:
initrd = build_initramfs(init, busybox, rc_text, args.root_password,
selinux=args.selinux, keep=args.keep_initramfs,
bundles=bundles)
root_img = None
print("initramfs:", initrd, f"({initrd.stat().st_size / 1024:.0f} KB)")
cmd = qemu_command(kernel, initrd, args, root_img)
print("$", " ".join(cmd))
return subprocess.run(cmd).returncode