Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 79 additions & 3 deletions build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import json
import os
import re
import stat
import subprocess
import tempfile
import time
Expand All @@ -13,7 +14,9 @@ STATUS = RUNTIME / "state-status.json"
ATTESTATION = RUNTIME / "boot-device.json"
BOOT_ID = Path("/proc/sys/kernel/random/boot_id")
PARTUUID_ROOT = Path("/dev/disk/by-partuuid")
SYS_DEV_BLOCK = Path("/sys/dev/block")
PARTUUID_RE = re.compile(r"^[0-9A-Fa-f-]{1,128}$")
MAJOR_MINOR_RE = re.compile(r"^[0-9]+:[0-9]+$")
E2FSCK_EXIT_RE = re.compile(
r"\be2fsck: exit (?:(?P<plain>[0-9]+)|Some\((?P<option>[0-9]+)\))(?=[:\s]|$)"
)
Expand Down Expand Up @@ -73,6 +76,70 @@ def e2fsck_exit_code(message: str) -> int | None:
return int(value)


def resolve_strict(path: Path) -> Path:
return path.resolve(strict=True)


def block_device_major_minor(path: Path) -> tuple[str | None, str | None]:
try:
metadata = os.stat(path)
except OSError as error:
return None, f"attested state path is unavailable: {error}"
if not stat.S_ISBLK(metadata.st_mode):
return None, "attested state path is not a block device"
return f"{os.major(metadata.st_rdev)}:{os.minor(metadata.st_rdev)}", None


def state_belongs_to_attested_disk(
state_major_minor: str, disk_major_minor: str
) -> tuple[bool, str | None]:
if not MAJOR_MINOR_RE.fullmatch(state_major_minor) or not MAJOR_MINOR_RE.fullmatch(
disk_major_minor
):
return False, "attested block major:minor is invalid"
try:
state_sysfs = resolve_strict(SYS_DEV_BLOCK / state_major_minor)
disk_sysfs = resolve_strict(SYS_DEV_BLOCK / disk_major_minor)
except (OSError, RuntimeError) as error:
return False, f"attested block sysfs identity is unavailable: {error}"
if state_sysfs.parent != disk_sysfs:
return False, "attested state block is not a child of the attested boot disk"
return True, None


def attested_state_device(
attestation: dict, state: dict
) -> tuple[Path | None, str | None]:
disk = attestation.get("disk")
if not isinstance(disk, dict):
return None, "attested boot disk identity is unavailable"
raw_path = state.get("path")
state_major_minor = state.get("major_minor")
disk_major_minor = disk.get("major_minor")
if not isinstance(raw_path, str):
return None, "attested state path is invalid"
path = Path(raw_path)
if len(path.parts) < 3 or path.parts[0] != "/" or path.parts[1] != "dev":
return None, "attested state path is outside /dev"
if not isinstance(state_major_minor, str) or not isinstance(disk_major_minor, str):
return None, "attested block major:minor is unavailable"
try:
device = resolve_strict(path)
except (OSError, RuntimeError) as error:
return None, f"attested state path is unavailable: {error}"
observed_major_minor, error = block_device_major_minor(device)
if error is not None or observed_major_minor is None:
return None, error or "attested state block identity is unavailable"
if observed_major_minor != state_major_minor:
return None, "attested state path major:minor changed"
belongs, error = state_belongs_to_attested_disk(
state_major_minor, disk_major_minor
)
if not belongs:
return None, error or "attested state block parent changed"
return device, None


def verified_state_device() -> tuple[Path | None, str | None]:
attestation = read_json(ATTESTATION)
try:
Expand All @@ -90,10 +157,19 @@ def verified_state_device() -> tuple[Path | None, str | None]:
partuuid = state.get("partuuid")
if not isinstance(partuuid, str) or not PARTUUID_RE.fullmatch(partuuid):
return None, "attested state PARTUUID is invalid"
attested_device, error = attested_state_device(attestation, state)
if error is not None or attested_device is None:
return None, error or "attested state device is unavailable"
try:
device = (PARTUUID_ROOT / partuuid).resolve(strict=True)
except OSError as error:
return None, f"attested state PARTUUID is unavailable: {error}"
partuuid_device = resolve_strict(PARTUUID_ROOT / partuuid)
except FileNotFoundError:
device = attested_device
except (OSError, RuntimeError) as error:
return None, f"attested state PARTUUID lookup failed: {error}"
else:
if partuuid_device != attested_device:
return None, "PARTUUID symlink resolved away from attested state device"
device = partuuid_device
mounted = subprocess.run(
["/usr/bin/findmnt", "--noheadings", "--source", str(device)],
check=False,
Expand Down
6 changes: 3 additions & 3 deletions build/usb/version.env
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
RIGOS_PRODUCT_VERSION=0.0.4-alpha.13
RIGOS_IMAGE_VERSION=0.0.4-alpha.13
RIGOS_PRODUCT_VERSION=0.0.4-alpha.14
RIGOS_IMAGE_VERSION=0.0.4-alpha.14
RIGOS_IMAGE_ID=rigos-usb-amd64
RIGOS_IMAGE_CHANNEL=alpha
RIGOS_BUILD_ORDINAL=13
RIGOS_BUILD_ORDINAL=14
RIGOS_XMRIG_VERSION=6.26.0
RIGOS_XMRIG_ARCHIVE_SHA256=fc6f8ae5f64e4f17481f7e3be29a1c56949f216a998414188003eae1db20c9e5
RIGOS_XMRIG_BINARY_SHA256=b20f39fc00d242e706b6c30367ad811c676e0575050a4ec2f30104b696944b49
4 changes: 2 additions & 2 deletions crates/rigos-config/tests/diagnostic_ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ fn diagnostic_ssh_keeps_host_identity_mandatory_without_requiring_ready_state()
);
}

assert!(version.contains("RIGOS_IMAGE_VERSION=0.0.4-alpha.13"));
assert!(version.contains("RIGOS_BUILD_ORDINAL=13"));
assert!(version.contains("RIGOS_IMAGE_VERSION=0.0.4-alpha.14"));
assert!(version.contains("RIGOS_BUILD_ORDINAL=14"));
}

#[test]
Expand Down
8 changes: 4 additions & 4 deletions crates/rigos-config/tests/firstboot_image_gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ fn repo_file(path: &str) -> String {
}

#[test]
fn alpha13_build_runs_source_and_exact_image_firstboot_gates() {
fn alpha14_build_runs_source_and_exact_image_firstboot_gates() {
let version = repo_file("build/usb/version.env");
let entrypoint = repo_file("scripts/build-usb-image-entrypoint.sh");
let verifier = repo_file("scripts/verify-firstboot-image.sh");
let hook = repo_file("build/usb/hooks/010-rigos.chroot");

assert!(version.contains("RIGOS_PRODUCT_VERSION=0.0.4-alpha.13"));
assert!(version.contains("RIGOS_IMAGE_VERSION=0.0.4-alpha.13"));
assert!(version.contains("RIGOS_BUILD_ORDINAL=13"));
assert!(version.contains("RIGOS_PRODUCT_VERSION=0.0.4-alpha.14"));
assert!(version.contains("RIGOS_IMAGE_VERSION=0.0.4-alpha.14"));
assert!(version.contains("RIGOS_BUILD_ORDINAL=14"));

assert!(entrypoint.contains("--test firstboot_tty"));
assert!(entrypoint.contains("--test state_resize_recovery"));
Expand Down
66 changes: 66 additions & 0 deletions crates/rigos-config/tests/state_resize_recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ fn state_resize_timeout_has_a_bounded_verified_recovery_path() {
"resize2fs: timeout",
"[\"/usr/sbin/e2fsck\", \"-f\", \"-y\"",
"e2fsck_exit == E2FSCK_UNCORRECTED_EXIT",
"SYS_DEV_BLOCK = Path(\"/sys/dev/block\")",
"MAJOR_MINOR_RE = re.compile",
"def attested_state_device(",
"stat.S_ISBLK",
"state_sysfs.parent != disk_sysfs",
"PARTUUID symlink resolved away from attested state device",
] {
assert!(
orchestrator.contains(required),
Expand Down Expand Up @@ -64,6 +70,7 @@ fn resize_timeout_recovery_retries_core_and_repairs_only_e2fsck_exit_four() {
fs::create_dir_all(&root).unwrap();

let fixture = r#"
import json
import runpy
import sys
from pathlib import Path
Expand All @@ -83,6 +90,65 @@ assert g['e2fsck_exit_code']('e2fsck: exit Some(4): inconsistency') == 4
assert g['e2fsck_exit_code']('e2fsck: exit Some(8): operational error') == 8
assert g['e2fsck_exit_code']('e2fsck: exit None: signal') is None

# The attested-path fallback requires exact path, major:minor, and disk parent identity.
attestation = {
'schema': 'rigos.boot-device/v1',
'boot_id': 'boot-test',
'verification_outcome': 'verified',
'disk': {'path': '/dev/test-disk', 'major_minor': '8:16'},
'state': {
'path': '/dev/test-state',
'major_minor': '8:20',
'partuuid': '5249474f-04',
},
}
state = attestation['state']
real_resolve_strict = g['resolve_strict']
g['resolve_strict'] = lambda _path: Path('/dev/test-state')
g['block_device_major_minor'] = lambda _path: ('8:20', None)
g['state_belongs_to_attested_disk'] = lambda state_mm, disk_mm: (
state_mm == '8:20' and disk_mm == '8:16',
None,
)
device, error = g['attested_state_device'](attestation, state)
assert device == Path('/dev/test-state')
assert error is None
g['block_device_major_minor'] = lambda _path: ('8:21', None)
device, error = g['attested_state_device'](attestation, state)
assert device is None
assert error == 'attested state path major:minor changed'
g['resolve_strict'] = real_resolve_strict

# Missing PARTUUID link may use only the already-verified attested block path.
links = root / 'by-partuuid'
links.mkdir()
g['PARTUUID_ROOT'] = links
g['attested_state_device'] = lambda _attestation, _state: (Path('/dev/test-state'), None)
g['ATTESTATION'].write_text(json.dumps(attestation), encoding='utf-8')
class Result:
def __init__(self, returncode, stdout=''):
self.returncode = returncode
self.stdout = stdout
self.stderr = ''
def fake_run(argv, **_kwargs):
if argv[0] == '/usr/bin/findmnt':
return Result(1)
if argv[0] == '/usr/sbin/blkid':
return Result(0, 'TYPE=ext4\nLABEL=RIGOS_STATE_SEED\nPARTUUID=5249474f-04\n')
raise AssertionError(f'unexpected command: {argv}')
g['subprocess'].run = fake_run
device, error = g['verified_state_device']()
assert device == Path('/dev/test-state')
assert error is None

# An existing PARTUUID link that points elsewhere is rejected, never used as fallback.
other = root / 'other-state'
other.touch()
(links / '5249474f-04').symlink_to(other)
device, error = g['verified_state_device']()
assert device is None
assert error == 'PARTUUID symlink resolved away from attested state device'

# A core resize timeout is completed under the longer verified repair budget,
# then core is rerun to perform the normal mount and initialization path.
statuses = [
Expand Down
17 changes: 13 additions & 4 deletions scripts/verify-state-recovery-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ release="$root/etc/rigos-release"
[[ -f "$release" ]] || die 'release metadata is missing'

python3 -m py_compile "$orchestrator" "$recovery" "$gate"
grep -Fqx 'VERSION_ID="0.0.4-alpha.13"' "$release" \
|| die 'embedded alpha.13 version is missing'
grep -Fqx 'VERSION_ID="0.0.4-alpha.14"' "$release" \
|| die 'embedded alpha.14 version is missing'
grep -Fqx 'TimeoutStartSec=20min' "$state_service" \
|| die 'state service full repair window is missing'

Expand All @@ -77,7 +77,13 @@ for required in \
'state filesystem resize failed' \
'resize2fs: timeout' \
'["/usr/sbin/e2fsck", "-f", "-y"' \
'e2fsck_exit == E2FSCK_UNCORRECTED_EXIT'
'e2fsck_exit == E2FSCK_UNCORRECTED_EXIT' \
'SYS_DEV_BLOCK = Path("/sys/dev/block")' \
'MAJOR_MINOR_RE = re.compile' \
'def attested_state_device(' \
'stat.S_ISBLK' \
'state_sysfs.parent != disk_sysfs' \
'PARTUUID symlink resolved away from attested state device'
do
grep -Fq "$required" "$orchestrator" \
|| die "state repair contract is missing: $required"
Expand All @@ -87,7 +93,10 @@ for required in \
'Some\((?P<option>[0-9]+)\)' \
'def revalidate_state_device(expected: Path)' \
'verified state device changed during repair' \
'or e2fsck_exit_code(check_failure) != E2FSCK_UNCORRECTED_EXIT'
'or e2fsck_exit_code(check_failure) != E2FSCK_UNCORRECTED_EXIT' \
'attested state path major:minor changed' \
'attested state block is not a child of the attested boot disk' \
'except FileNotFoundError:'
do
grep -Fq "$required" "$orchestrator" \
|| die "state repair safety boundary is missing: $required"
Expand Down