Files
fds-os/tests/integration/m1-checks
T
2026-09-21 22:29:23 +08:00

138 lines
6.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# Validate the exported tar, then corrupt disposable copies to test rejection.
source "$(dirname -- "${BASH_SOURCE[0]}")/../../tools/lib.sh"
cd "$FDS_ROOT"
[[ -f out/rootfs-aarch64.tar ]] || die 'Run make rootfs PROFILE=cli first'
scratch=$(mktemp -d "$FDS_ROOT/out/m1-checks.XXXXXX")
trap 'rm -rf -- "$scratch"' EXIT
build=$(dirname -- "$(realpath out/rootfs-aarch64.tar)")
(
cd "$build"
sha256sum -c rootfs.sha256 packages.sha256
)
python3 - "$build" <<'PY'
import json
import pathlib
import struct
import sys
import tarfile
build = pathlib.Path(sys.argv[1])
metadata = json.loads((build / 'archive-metadata.json').read_text())
with tarfile.open(build / 'rootfs-aarch64.tar') as archive:
members = {member.name: member for member in archive}
assert members['.'].isdir() and (members['.'].uid, members['.'].gid, members['.'].mode) == (0, 0, 0o755), 'Incorrect root directory metadata'
assert members.keys() == metadata.keys(), 'Archive member mismatch'
for name, member in members.items():
assert not name.startswith('/') and '..' not in pathlib.PurePosixPath(name).parts
assert not (member.ischr() or member.isblk()), name
if member.isfifo():
assert name.startswith('etc/s6-linux-init/current/run-image/'), name
expected = metadata[name]
assert (member.uid, member.gid, member.mode) == (expected['uid'], expected['gid'], int(expected['mode'], 8)), name
for name in ('usr/bin/wall', 'usr/bin/write'):
if name in members:
assert (members[name].uid, members[name].gid, members[name].mode) == (0, 5, 0o2755), name
assert (members['usr/lib/utempter/utempter'].uid, members['usr/lib/utempter/utempter'].gid, members['usr/lib/utempter/utempter'].mode) == (0, 14, 0o2711), 'Incorrect utempter group restoration'
assert members['etc/shadow'].mode == 0o600
assert members['tmp'].mode == 0o1777
assert members['root'].mode == 0o700
assert (members['usr/bin/xbps-uchroot'].uid, members['usr/bin/xbps-uchroot'].gid, members['usr/bin/xbps-uchroot'].mode) == (0, 101, 0o4750)
assert members['bin'].linkname == 'usr/bin'
capability = members['usr/bin/iputils-ping'].pax_headers['SCHILY.xattr.security.capability'].encode('utf-8', 'surrogateescape')
assert len(capability) == 20 and struct.unpack_from('<I', capability)[0] >> 24 == 2
assert struct.unpack_from('<I', capability, 4)[0] & (1 << 13), 'Missing CAP_NET_RAW'
print('PASS: archive paths, ownership, modes and portable ping capability')
PY
mkdir "$scratch/capability-restore"
# Exercise GNU tar's actual xattr restoration inside a private user namespace.
# The resulting v3 xattr is namespaced to this host user, not host root.
bwrap --unshare-user --uid 0 --gid 0 --cap-add CAP_SETFCAP \
--ro-bind / / --bind "$scratch/capability-restore" "$scratch/capability-restore" \
--chdir "$scratch/capability-restore" \
tar --extract --file "$FDS_ROOT/out/rootfs-aarch64.tar" --numeric-owner \
--same-permissions --xattrs --xattrs-include=security.capability usr/bin/iputils-ping
python3 - "$scratch/capability-restore/usr/bin/iputils-ping" <<'PY'
import os
import struct
import sys
cap = os.getxattr(sys.argv[1], 'security.capability')
assert len(cap) == 24 and struct.unpack_from('<I', cap)[0] >> 24 == 3
assert struct.unpack_from('<I', cap, 20)[0] == os.getuid()
assert struct.unpack_from('<I', cap, 4)[0] & (1 << 13)
print('PASS: GNU tar restores ping capability in a private user namespace')
PY
mkdir "$scratch/root"
# Extract as the current user, preserving target modes for the audit. Capabilities
# are verified above; this test does not grant capabilities to host-owned files.
tar --extract --file out/rootfs-aarch64.tar --directory "$scratch/root" \
--no-same-owner --same-permissions
python3 tools/rootfs-audit "$scratch/root"
tools/rootfs-runtime-check "$scratch/root"
python3 - "$scratch/root" <<'PY'
import importlib.machinery
import importlib.util
import pathlib
import plistlib
import shutil
import sys
sys.path.insert(0, 'tools')
loader = importlib.machinery.SourceFileLoader('audit_module', 'tools/rootfs-audit')
spec = importlib.util.spec_from_loader(loader.name, loader)
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
root = pathlib.Path(sys.argv[1])
database = root / 'var/db/xbps/pkgdb-0.38.plist'
original = database.read_bytes()
db = plistlib.loads(original)
def reject(label, needle):
try:
module.audit(root)
except AssertionError as error:
assert needle in str(error), (label, error)
print('PASS: rejects ' + label)
else:
raise AssertionError('Incorrectly accepted: ' + label)
db['busybox'] = {'architecture': 'aarch64', 'state': 'installed'}
database.write_bytes(plistlib.dumps(db))
reject('forbidden package', 'Forbidden package')
database.write_bytes(original)
db = plistlib.loads(original)
db['glibc']['state'] = 'unpacked'
database.write_bytes(plistlib.dumps(db))
reject('unfinished package configuration', 'Unconfigured package')
database.write_bytes(original)
db = plistlib.loads(original)
db['glibc']['architecture'] = 'aarch64-musl'
database.write_bytes(plistlib.dumps(db))
reject('wrong package ABI', 'Wrong package ABI')
database.write_bytes(original)
binary = root / 'usr/bin/ls'
backup = binary.read_bytes()
binary.write_bytes(pathlib.Path('/usr/bin/ls').read_bytes())
reject('host ELF contamination', 'Non-aarch64 ELF')
binary.write_bytes(backup)
marker = root / 'etc/s6-rc/source/boot/contents.d/dasungd'
marker.unlink()
reject('missing base Dasung service', 'Dasung missing')
marker.touch()
init = root / 'etc/s6-linux-init/current/bin/init'
original_init = init.read_bytes()
init.write_text('#!/bin/bash\nexec /usr/bin/s6-svscan /run/service\n')
reject('shell PID 1 launcher', 'Init is not the native s6 launcher')
init.write_bytes(original_init)
boot_member = root / 'etc/s6-rc/source/boot/contents.d/getty'
boot_member.unlink()
reject('missing console boot member', 'Missing boot service: getty')
boot_member.touch()
PY
if PROFILE=windowmaker tools/build-rootfs >"$scratch/rejection.log" 2>&1; then
die 'Unimplemented profile was accepted'
fi
grep -q 'Supported profiles: cli, development' "$scratch/rejection.log"
printf 'PASS: rejects unimplemented image profile\n'
printf 'NOTE: run make init-test separately for native PID 1 and full ARM boot checks\n'
printf 'SKIP: physical Pi/monitor tests and boot timings require hardware\n'
printf 'PASS: exported-rootfs acceptance and failure-path checks\n'