Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
8724c74
feat(runtime): bind native foreground publication to host boot
Oct 8, 2026
880e253
style(runtime): format native publication decoder
Oct 8, 2026
8633485
fix(runtime): preserve native cleanup progress on retry
Oct 8, 2026
e1deb22
fix(runtime): persist fresh retry terminal evidence before delete
Oct 8, 2026
eeb6b93
feat(runtime): select exact dead native foreground recovery evidence
Oct 8, 2026
77a43df
test(runtime): bind recovery file replacement to saved selection
Oct 8, 2026
055294f
fix(runtime): respect bounded recovery intent reader
Oct 8, 2026
74d01dd
fix(runtime): retain cleanup guards around engine calls
Oct 8, 2026
9f425b5
fix(runtime): guard cleanup journal publications
Oct 8, 2026
525d554
feat(runtime): expose read-only native recovery selection
Oct 8, 2026
7470a5d
test(runtime): bind privacy and lost-guard evidence precisely
Oct 8, 2026
778520e
chore: reconcile native recovery source with next
Oct 8, 2026
9dc79b7
feat(runtime): recover exact same-boot native publications
Oct 8, 2026
113b4da
fix(runtime): bind native recovery environment completeness
Oct 8, 2026
397dcc8
feat(runtime): expose exact native owner recovery command
Oct 8, 2026
8a55df2
test(runtime): bound native recovery argument refusal checks
Oct 8, 2026
d29c97a
feat(cli): decode closed native recovery selections
Oct 8, 2026
0fd94f7
chore: reconcile native recovery with shipping next
Oct 8, 2026
7400c21
feat: bind explicit frontend lock recovery to private selection
Oct 8, 2026
466646c
fix: recheck selected frontend lock before retirement
Oct 8, 2026
ef1e807
feat: bound explicit native recovery request transport
Oct 8, 2026
436fd36
test: preserve Bun spawn overload in recovery inspection control
Oct 8, 2026
476c09f
test: forward recovery inspection spawn through exact overload
Oct 8, 2026
77af0ca
feat: retain issued frontend recovery lease through retirement
Oct 8, 2026
e3a2a92
fix: revoke frontend recovery lease before final deletion
Oct 8, 2026
c52ec8b
merge: reconcile native recovery with current next
Oct 8, 2026
a429527
feat: add explicit native frontend recovery owner
Oct 8, 2026
940f63f
fix: bind recovery lease candidates before publication
Oct 8, 2026
c59453d
fix: retain fresh recovery owner on release refusal
Oct 8, 2026
8ed1fb8
fix: preserve recovery result literal types
Oct 8, 2026
1393d55
test: model native frontend recovery lease ordering
Oct 8, 2026
c0e2b16
Merge commit '5794076a0bd7465bef917187614bc64f28fa06e3' into feat/nat…
Oct 8, 2026
f908234
test: delimit conditional recovery model values
Oct 8, 2026
929e817
test: preserve native unsupported dispatch contract
Oct 8, 2026
d21f97b
test: bind recovery model unsafe state assignments
Oct 8, 2026
67f521d
test: register bounded frontend recovery model controls
Oct 8, 2026
896502a
test(native): isolate short status deadlines and explicit modes
Oct 8, 2026
41204e7
test(native): retain uncertain keeper completion evidence
Oct 8, 2026
cb4f821
test(native): record selected status fixture stages
Oct 8, 2026
9d7d662
test(native): retain fixtures across diagnostic failures
Oct 8, 2026
d1af34d
test: admit held status pipes before cancellation
Oct 8, 2026
56c3b18
test: bind pending status cancellation witness
Oct 8, 2026
84adcd7
chore: reconcile native recovery with current next
Oct 8, 2026
936ecce
test: observe native startup deadline phases
Oct 8, 2026
93af03b
chore: retain canonical relay fixture correction
Oct 8, 2026
38eb404
test: bind startup deadline checks to observed phases
Oct 8, 2026
a02622a
test: preserve buffered ready deadline refusal
Oct 8, 2026
329828e
test: narrow observed startup error and keeper identity
Oct 8, 2026
1a716cd
chore: reconcile native recovery with current next
Oct 8, 2026
e6ad592
test: expose dead relay publication refusal predicates
Oct 8, 2026
cd2df7f
fix(native): bind foreground owners to stable boot sessions
Oct 9, 2026
73c7483
test(native): fence final boot observation and codec admission
Oct 9, 2026
bbb8087
fix(native): require exact boot UUID wire length
Oct 9, 2026
87425d1
fix: preserve omitted profiles for native recovery
Oct 9, 2026
d36587d
merge: reconcile native recovery with current next
Oct 9, 2026
6d82324
merge: reconcile stable boot ownership with recovery profile fix
Oct 9, 2026
7d92ea8
chore: reconcile boot session identity with next
Oct 9, 2026
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
11 changes: 9 additions & 2 deletions docs/reference/native-authored-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ and refuses replay; a missing ready mapping does not authorize a fresh start.

`down --recover` explicitly retires a complete stored native generation whose
foreground publisher and frontend admission owner are both dead on the same host
boot. It requires the current version 3 native publication, the exact stored Ready,
boot. New version 4 native publications use the kernel boot-session UUID, which
remains independent of calendar clock correction. Recovery requires the exact stored Ready,
startup intent and private source envelope, and the original guest incarnation.
Live owners, older version 2 dead publications, partial startup, pending writes,
changed files and rebooted guests refuse. Env/profile changes, service subsets and
Expand All @@ -47,6 +48,12 @@ nor acquires managed values or starts a provider.
Omit `--profile` to recover the stored generation; explicit empty or named profile
overrides both refuse.

The closed version 3 publication and version 1 recovery selector remain supported
with their original calendar boot qualifier; clock drift conservatively refuses
that legacy recovery. New version 4 owners use version 2 UUID selectors. Missing,
malformed or unavailable UUIDs refuse; no old record is migrated or given inferred
session authority. PID birth/executable, peer, inode, gate and run-lock checks remain.

Recovery commits a distinct private intent before calling the Rust cleanup owner.
The original raw selectors and resource inventory remain fixed through retries;
current cleanup phases may advance. Before retiring each frontend file, the owner
Expand All @@ -69,4 +76,4 @@ this operation does not repair arbitrary partial lock or file publications.
The native receipt and source paths are distinct from strict Compose v1 artifacts.
No native hash substitutes for a normalized Compose hash. Source and fake-driver
checks do not qualify an installed frontend, a live provider, the full authored
corpus, actual version 3 dead-owner recovery or resource overhead; those remain separate gates.
corpus, actual dead-owner recovery or resource overhead; those remain separate gates.
33 changes: 25 additions & 8 deletions packages/runtime-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,29 +114,46 @@ this same subset. Its private `native-graph-owner` record and authenticated v2
`native-graph-control` status/cleanup protocol bind the native review, exact
process incarnation, directory, socket, file and retained per-run lock. Direct
run/cleanup and foreground publication exclude each other under that lock.
New publications use a closed version3 owner record with independently captured
native host boot time. Live authentication rechecks that boot together with the
existing exact process and filesystem identities. The closed version2 live-owner
decoder remains available without inferring host boot from PID birth or guest
receipt boot. Version2 records reject the new field, and version3 records require
it. Native runtime receipts and the authenticated control/ready wire remain v2;
New publications use a closed version4 owner with `host_boot_uuid`, captured by
the native read-only `kern.bootsessionuuid` sysctl. Its fixed 37-byte response must
contain a nonzero ASCII UUID and terminal NUL; the reader canonicalizes lowercase.
Persisted UUIDs must already be lowercase. Unavailable or malformed readings refuse,
without a timestamp or shell fallback. Live authentication rechecks the session
together with the exact nonzero process incarnation and filesystem identities;
it does not compare process birth against mutable calendar boot time.
The closed version2 live-owner decoder remains available without a boot qualifier.
Version3 remains strictly qualified by its original `host_boot_micros`; it receives
no inferred UUID or migration. Each version rejects the other versions' qualifiers.
Native runtime receipts and the authenticated control/ready wire remain v2;
Compose receipt and owner formats remain unchanged. This provenance alone grants
no dead-owner recovery authority.
The inactive read-only recovery selector admits only a complete Ready journal and
a dead version3 publication on the same native host boot. It captures the current
a dead version3 or version4 publication on the same qualified native host boot. It captures the current
private owner bytes and inode; its raw SHA is a selector for later independent
admission, not an external identity anchor from before that capture. After a
durable intent, the saved inode and digest refuse replacement. Cleanup progress
retains the original Ready selectors and exact inventory. Pending writes refuse;
retired paths require the intent's preceding phase and unchanged archived inode.
`graph native recovery-selection --run-id ID --json` exposes only the closed
selection: run, original value-free Ready receipt, raw receipt/owner hashes and
native host boot time. The receipt contains review hashes, workload readiness,
the original boot qualifier. Version1 selections retain version3 calendar micros;
version2 selections carry version4 `host_boot_uuid`. Neither rewrites original
owner bytes, inode or digest. Version2 dead owners remain ineligible. Version3
recovery conservatively refuses if its calendar qualifier has drifted.
The receipt contains review hashes, workload readiness,
resource IDs/images/networks and terminal observations; it contains no environment
keys/values, command argv, source bytes or publication process/path metadata. The
selector creates no intent and connects no provider. It grants no cleanup or
frontend recovery authority.

The UUID boundary addresses calendar correction without weakening reboot refusal.
[XNU calendar updates adjust boot time](https://github.com/apple-oss-distributions/xnu/blob/main/osfmk/kern/clock.c#L674-L752),
while the [read-only boot-session sysctl](https://github.com/apple-oss-distributions/xnu/blob/main/bsd/kern/kern_sysctl.c#L2735-L2762)
is [generated at boot](https://github.com/apple-oss-distributions/xnu/blob/main/iokit/Kernel/IOPMrootDomain.cpp#L3846-L3861).
Unrelated legacy filesystem chronology still uses its existing calendar helper.
Injected-reader and codec regressions qualify the ownership fences without changing
the host clock or rebooting. They do not establish actual provider recovery.

The separate explicit library recovery entrypoint re-admits both raw selectors
and commits a bounded 64 KiB recovery intent before cleanup. It retains the
publication gate, original operation lock and cleanup provider lease through
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use super::*;
use crate::provider::graph::foreground::{signals, transport};
use serde::{Deserialize, Serialize};
use std::{cell::Cell, io::Write};
mod host_boot;
mod owner;
pub mod recovery;
pub(super) use owner::DirectGuard;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//! Stable native host boot authority. Calendar boot time is not an incarnation ID.
use super::{CandidateError, refused};
use serde::{Deserialize, Deserializer, Serialize};

/// Persisted boot qualifiers are canonical, nonzero UUIDs. No timestamp fallback.
#[derive(Clone, PartialEq, Eq, Serialize)]
#[serde(transparent)]
pub(super) struct Session(String);
impl Session {
fn parse(value: &str) -> Option<Self> {
let bytes = value.as_bytes();
if bytes.len() != 36
|| bytes.iter().enumerate().any(|(i, byte)| {
if matches!(i, 8 | 13 | 18 | 23) {
*byte != b'-'
} else {
!byte.is_ascii_digit() && !(b'a'..=b'f').contains(byte)
}
})
|| !bytes
.iter()
.any(|byte| matches!(byte, b'1'..=b'9' | b'a'..=b'f'))
{
return None;
}
Some(Self(value.into()))
}
}
impl<'de> Deserialize<'de> for Session {
fn deserialize<D: Deserializer<'de>>(reader: D) -> Result<Self, D::Error> {
let value = String::deserialize(reader)?;
Self::parse(&value).ok_or_else(|| serde::de::Error::custom("Invalid host boot qualifier"))
}
}

fn decode(buffer: &[u8; 37], length: usize) -> Result<Session, CandidateError> {
if length != buffer.len() || buffer[36] != 0 {
return Err(refused());
}
let text = std::str::from_utf8(&buffer[..36]).map_err(|_| refused())?;
// XNU publishes uppercase UUID text; only the native reader normalizes it.
Session::parse(&text.to_ascii_lowercase()).ok_or_else(refused)
}

pub(super) fn read() -> Result<Session, CandidateError> {
#[cfg(test)]
if let Some(value) = test::next() {
return value.ok_or_else(refused);
}
let mut buffer = [0u8; 37];
let mut length = buffer.len();
// SAFETY: the read-only sysctl writes at most this fixed initialized buffer;
// its size pointer is live, newp is null, and no pointer is retained.
if unsafe {
libc::sysctlbyname(
c"kern.bootsessionuuid".as_ptr(),
buffer.as_mut_ptr().cast(),
&mut length,
std::ptr::null_mut(),
0,
)
} != 0
{
return Err(refused());
}
decode(&buffer, length)
}

#[cfg(test)]
pub(super) mod test {
use super::*;
use std::{cell::RefCell, collections::VecDeque};
thread_local! {
static OVERRIDE: RefCell<Option<VecDeque<Option<Session>>>> = const { RefCell::new(None) };
}
pub(super) fn next() -> Option<Option<Session>> {
OVERRIDE.with(|cell| {
let mut state = cell.borrow_mut();
let values = state.as_mut()?;
if values.len() > 1 {
values.pop_front()
} else {
values.front().cloned()
}
})
}
pub(in super::super) struct Guard(Option<VecDeque<Option<Session>>>);
impl Guard {
pub(in super::super) fn set(value: Option<&str>) -> Self {
Self::sequence(&[value])
}
pub(in super::super) fn sequence(values: &[Option<&str>]) -> Self {
assert!(!values.is_empty());
Self(OVERRIDE.with(|cell| {
cell.replace(Some(
values
.iter()
.map(|value| value.map(|v| Session::parse(v).unwrap()))
.collect(),
))
}))
}
}
impl Drop for Guard {
fn drop(&mut self) {
OVERRIDE.with(|cell| cell.replace(self.0.take()));
}
}
#[test]
fn fixed_native_buffer_normalizes_only_valid_exact_uuid() {
let mut buffer = *b"12345678-ABCD-ABCD-ABCD-123456789ABC\0";
assert!(
decode(&buffer, 37).unwrap()
== Session::parse("12345678-abcd-abcd-abcd-123456789abc").unwrap()
);
for length in [0, 36, 38] {
assert!(decode(&buffer, length).is_err());
}
buffer[36] = b'x';
assert!(decode(&buffer, 37).is_err());
for value in [
"00000000-0000-0000-0000-000000000000",
"12345678-abcd-abcd-abcd-123456789abg",
"12345678_abcd-abcd-abcd-123456789abc",
] {
let bytes: [u8; 37] = format!("{value}\0").into_bytes().try_into().unwrap();
assert!(decode(&bytes, 37).is_err());
}
buffer = *b"12345678-ABCD-ABCD-ABCD-123456789ABC\0";
buffer[0] = 0xff;
assert!(decode(&buffer, 37).is_err());
buffer[0] = 0;
assert!(decode(&buffer, 37).is_err());
}
#[test]
fn persisted_uuid_is_closed_and_reader_unavailability_never_falls_back() {
for value in [
serde_json::Value::Null,
serde_json::json!(1),
serde_json::json!("12345678-ABCD-ABCD-ABCD-123456789ABC"),
serde_json::json!("00000000-0000-0000-0000-000000000000"),
] {
assert!(serde_json::from_value::<Session>(value).is_err());
}
let _guard = Guard::set(None);
assert!(read().is_err());
}
}
Loading
Loading