Repository navigation
The AML interpreter: a machine's DSDT and SSDTs loaded into one namespace and evaluated, bounded in steps, depth, size and sum - #739
Conversation
…ested userland/acpiserver/aml (package toyos-aml) loads a machine's DSDT and SSDTs into one namespace and evaluates its objects: the interpreter stage of issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md. It is written from the ACPI Specification 6.5 alone (chapter 20's grammar, §5.3-5.5's namespace, loading and method execution, §19.3.5's conversions and §19.6's operators), cited by section at each rule. It sits inside the ACPI server, its first user by the track, as the compositor's desktop and the soundserver's mixer sit inside theirs; the server itself is stage 1's, on its own branch. A definition block is interpreted as it is read, at load as a method is at its invocation (§5.4.2), so a name in an argument position is resolved when reached and a method invocation takes the arguments its method declares. Hardware is reached only through the caller's Host: SystemMemory, SystemIO, PCI_Config (by _ADR, _BBN and _SEG) and EmbeddedControl. Every evaluation is bounded in steps, nesting, object size and time asked to sleep, and malformed bytes are refused by name. _OSI answers as the owner ruled, like Windows: yes to every Windows version string Microsoft publishes, no to anything else. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C9qc7GuMaHZ9xhKsZ57RVz
|
Negative-control patches behind the body's table, each applied to How each control ran:
After the last control, Every control built (build EXIT=0) and failed the suite (test EXIT=101).
Generated by Claude Code |
|
Review of #739 at Net: 15 files, +4851/−0. Production: +3086 ( Merge: I did not run the host gate or the mutation controls. My brief asked for both, but my role prompt says "you run no test and no build". The orchestrator owes both measurements; see BLOCKER 1 for the host gate. I checked the ten controls by reading only: each patch in the comment breaks exactly what its named test asserts. Their run log is the comment's, at BLOCKER
NOTE
The implementer's readings
SEND BACK |
…DSDT loads Review round 1, BLOCKERs 2 and 10. The owner ruled on 2026-10-05 to accept what real firmware ships: "Parse Processor and other legacy constructs real tables still contain, per their last spec definition". ProcessorOp now parses by ACPI 6.3 Errata A, the last edition to define it (§20.2.5.2, §19.6.108): a named object that opens a scope, ObjectType 12 (Table 19.36), a Notify target. QEMU 11.1.1's DSDT, already in the tree as toyos-acpi/fixtures/qemu-11.1.1/dsdt.bin, holds two; it now loads, and its \_S5 gives SLP_TYPa 0, what its boot logged. A load takes a toyos_acpi::Table, whose open checks §5.2.6's length and checksum for every table; the interpreter's own copy of that check goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C9qc7GuMaHZ9xhKsZ57RVz
…reference chains Review round 1, BLOCKERs 3 to 8, and the NOTE on generations. - Every string, buffer and package is made by one of the machine's constructors, which bound its size (1 MiB, or 65536 elements), charge a step for every 64 bytes of it, and hold it against the interpreter's Meter until it drops: what one interpreter holds live is bounded at 16 MiB in sum. A conversion refuses an output too large before it builds it. - A step is charged in proportion to work: bytes made, copied or compared, a bit walked in a buffer field, a byte written out as digits. The worst evaluation measured, a buffer field of 8 Mi bits read in a loop, ends refused in 110 ms (release) or 1.46 s (debug). - A reference to a package element or to a LocalX or ArgX lives only in a LocalX or ArgX, which a method's exit clears: storing one into a package or a named object is refused, so no chain of references forms and no cycle outlives its evaluation. - An Alias is an object its table or method created like any other: a refused load and a method's exit remove it. - _BBN above 0xFF, _SEG above 0xFFFF, an _ADR naming no single function and a SystemIO port past 0xFFFF are refused, not truncated. - A namespace slot whose generation would wrap is retired. - \_OS is "Microsoft Windows NT", as the owner ruled on 2026-10-05. Each construction the review names has a hostile test, red before this commit: conversion output and proportional steps timed out at 120 s, the live fill and the reference chain aborted, the Alias and address tests failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C9qc7GuMaHZ9xhKsZ57RVz
…not two Review round 1, BLOCKER 9. Mutation (c), the generation check dropped, stayed green: the test's reusing object had gone with its method before the old reference was followed, so the slot was dead either way. MAIN now defines the reusing Name itself, alive when the reference is followed. Mutation (d), ConcatenateResTemplate's own bounded(), stayed green because the constructor bounds every object it makes; the operator's copy goes, and deleting the constructor's bound turns the test red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C9qc7GuMaHZ9xhKsZ57RVz
…stage Review round 1, NOTEs and the implementer's readings. The track records the owner's rulings of 2026-10-05 verbatim (\_OS, _OSI feature groups, old opcodes) and the clean-room ruling as the orchestrator's brief carried it, and gains an interpreter stage whose exit a test and a run against the T14's tables can read. - _OSI answers every ACPI feature group no, which is how Windows answers: Microsoft's page says "Windows supports _OSI only for the use of identifying the host version of Windows". The interpreter so claims nothing it does not carry. - "Refuse only what is truly malformed": a multi-byte PkgLength's reserved bits, a zero-segment MultiNamePath, string bytes above AsciiChar, FieldFlags bit 7, SyncFlags' reserved bits and External's ArgumentCount no longer refuse a table. A reserved address space, AccessType or UpdateRule loads and is refused where an access needs its meaning. - One parser reads a NameString written as text, for DerefOf and for the caller; one function follows a reference to its object, for ObjectType, SizeOf and Concatenate's type names. - The contract states the stack an evaluation needs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C9qc7GuMaHZ9xhKsZ57RVz
|
Answer to review round 1, at head BLOCKERs
NOTEs
Readings
Generated by Claude Code |
|
Review of #739 at Net: 16 files, +5332/−0. Production: +3283 ( Merge: Owner's rulings: the three 2026-10-05 quotes in the track match the orchestrator's record of them character for character. Round 1 BLOCKERs
BLOCKER
NOTE
SEND BACK |
…r counts tables and names Review round 2 of #739, an outside review of the same head, and the run against the T14's tables that round 1 left owed. A reference to a LocalX or ArgX held its slot by an Rc, so one returned from a method outlived the frame, could be stored through, and chained or pointed at itself: 50,000 iterations of the review's self-store left 2.4 MB behind per evaluation. Ref::Slot is now a Weak: the frame alone holds its slots, and a reference whose method has exited names nothing, as a NodeId whose object is gone does. Frame::clear, which emptied the slots to break cycles among them, has nothing left to break and goes. A String stored to a field unit built one field-sized Vec per character before writing any: 98,303 characters into a field of a mebibyte asked for 96 GiB, and under a 1 GiB heap cap the process died with "memory allocation of 1048576 bytes failed". write_field now hands write_units slices of the source, which reads past a slice's end as zeros; nothing is built, and the same store ends at the step bound with a peak heap of 168 KiB. The Meter no longer hides an under-count (give panics), Data::bits hands out a slice, and the Global Lock count and the predefined objects fail fast in the same way. The Meter counted strings, buffers and packages only. Measured at the old head: 24 tables of a mebibyte, each kept by one method, were all held, 25.2 MB and no refusal; one table naming 204,000 field units held 42.6 MB. A loaded table is now a metered Bytes, and every namespace node is taken from the Meter at creation and given back at removal. The T14's tables, read from outside the tree, found two things the interpreter refused that firmware ships. An AnyAcc field took the narrowest natural unit that held it even where that unit ran past its region's end, which refused a 16-bit field in the last two bytes of a 13-byte region; it now takes such a unit only inside the region, else bytes. A PCI_Config region reached its host bridge only from directly below it; a device below a bridge is now on the bus the bridge's Secondary Bus Number register names. load's "shorter than its header" branch was unreachable behind Table::open and goes. QEMU's DSDT is asserted whole and against toyos_acpi::s5_slp_typ, and two of its methods run against the registers they read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Round 3 negative control and mutations, at Each was a patch checked with m0, the negative control: the whole source change reverted (
m1, an AnyAcc unit may leave its region. Build diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 7611be298..e5512ed68 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -119,7 +119,7 @@ impl Machine<'_> {
.into_iter()
.find(|&w| {
let unit = f.bit / (8 * w);
- unit == (f.bit + f.len - 1) / (8 * w) && within((unit + 1) * w)
+ unit == (f.bit + f.len - 1) / (8 * w) && within(0)
})
.unwrap_or(1),
1 => 1,m2, namespace nodes are not metered. Build diff --git a/userland/acpiserver/aml/src/namespace.rs b/userland/acpiserver/aml/src/namespace.rs
index c9b91e7c5..684b46a85 100644
--- a/userland/acpiserver/aml/src/namespace.rs
+++ b/userland/acpiserver/aml/src/namespace.rs
@@ -140,7 +140,7 @@ impl Namespace {
live: true,
};
let fresh = u32::try_from(self.nodes.len()).map_err(|_| Error::Bound("the namespace's node count"))?;
- self.meter.take(NODE)?;
+ self.meter.take(NODE - NODE)?;
let id = match self.free.pop() {
Some(index) => {
let slot = &mut self.nodes[index as usize];
@@ -182,7 +182,7 @@ impl Namespace {
};
n.live = false;
n.object = Object::Uninit;
- self.meter.give(NODE);
+ self.meter.give(NODE - NODE);
doomed.extend(core::mem::take(&mut n.children).into_values());
// A slot whose generation would wrap is retired, so no stale
// NodeId ever names a live object again.m3, a bridge is asked for its primary bus. Build diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 7611be298..e6ccba023 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -203,7 +203,7 @@ impl Machine<'_> {
let segment = u16::try_from(segment).map_err(|_| Error::Rule("a _SEG above 0xFFFF (§6.5.6)"))?;
for &above in path.iter().skip(1).rev() {
let b = self.function(above, segment, bus)?;
- let secondary = Address::PciConfig { segment, bus, device: b.device, function: b.function, offset: 0x19 };
+ let secondary = Address::PciConfig { segment, bus, device: b.device, function: b.function, offset: 0x18 };
bus = self.host.read(secondary, crate::Access::Byte).map_err(|d| Error::Host(d.0))? as u8;
}
let at = self.function(path.first().copied().unwrap_or(bridge), segment, bus)?;The script's own log: |
|
The out-of-tree check behind the body's T14 and heap numbers, at Commands, each
//! An out-of-tree check of `toyos-aml`: real tables read from a directory the
//! tree never holds, and the hostile constructions of review round 2, each
//! under an allocator that counts what the process holds live.
//!
//! Nothing here prints a byte of a table unless `--show` is given; without it
//! the output is counts, lengths, exit kinds and this interpreter's own
//! refusal texts.
#[path = "<worktree>/userland/acpiserver/aml/tests/common/mod.rs"]
mod common;
use std::alloc::{GlobalAlloc, Layout, System};
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
use common::*;
use toyos_aml::{Access, Address, Denied, Error, Host, Interpreter, Value};
struct Counting;
static LIVE: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);
static CAP: AtomicUsize = AtomicUsize::new(usize::MAX);
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, l: Layout) -> *mut u8 {
let now = LIVE.fetch_add(l.size(), Relaxed) + l.size();
if now > CAP.load(Relaxed) {
LIVE.fetch_sub(l.size(), Relaxed);
return std::ptr::null_mut();
}
PEAK.fetch_max(now, Relaxed);
unsafe { System.alloc(l) }
}
unsafe fn dealloc(&self, p: *mut u8, l: Layout) {
LIVE.fetch_sub(l.size(), Relaxed);
unsafe { System.dealloc(p, l) }
}
}
#[global_allocator]
static A: Counting = Counting;
fn live() -> usize {
LIVE.load(Relaxed)
}
/// A host that answers every read with zero and counts what it is asked.
#[derive(Default)]
struct Count {
reads: [u64; 4],
writes: [u64; 4],
sleeps: u64,
stalls: u64,
notifies: u64,
locks: u64,
/// What a SystemMemory read at an address answers instead of zero.
mem: BTreeMap<u64, u64>,
/// A range of memory, (start, length), answering one value.
range: Option<(u64, u64, u64)>,
}
fn space(a: Address) -> usize {
match a {
Address::Memory(_) => 0,
Address::Io(_) => 1,
Address::PciConfig { .. } => 2,
Address::EmbeddedControl(_) => 3,
}
}
impl Host for Count {
fn read(&mut self, at: Address, w: Access) -> Result<u64, Denied> {
if std::env::var("AML_TRACE").is_ok() {
println!(" read {at:x?} {w:?}");
}
self.reads[space(at)] += 1;
Ok(match at {
Address::Memory(a) => self.mem.get(&a).copied().or_else(|| self.range.filter(|r| a >= r.0 && a < r.0 + r.1).map(|r| r.2)).unwrap_or(0),
_ => 0,
})
}
fn write(&mut self, at: Address, w: Access, v: u64) -> Result<(), Denied> {
if std::env::var("AML_TRACE").is_ok() {
println!(" write {at:x?} {w:?} {v:#x}");
}
self.writes[space(at)] += 1;
Ok(())
}
fn sleep(&mut self, _: u64) {
self.sleeps += 1;
}
fn stall(&mut self, _: u64) {
self.stalls += 1;
}
fn timer(&mut self) -> u64 {
0
}
fn notify(&mut self, _: &str, _: u64) {
self.notifies += 1;
}
fn global_lock(&mut self, _: bool) -> Result<(), Denied> {
self.locks += 1;
Ok(())
}
}
impl Count {
fn line(&self) -> String {
format!(
"reads mem/io/pci/ec {:?} writes {:?} sleeps {} stalls {} notifies {} locks {}",
self.reads, self.writes, self.sleeps, self.stalls, self.notifies, self.locks
)
}
}
fn open_load(i: &mut Interpreter, h: &mut dyn Host, t: &[u8]) -> Result<(), Error> {
let signature: [u8; 4] = t[..4].try_into().unwrap();
let table = toyos_acpi::Table::open(Image(t), 0, &signature, 0).map_err(|_| Error::Table("Table::open refused it"))?;
i.load(h, &table)
}
/// An outcome with nothing of the table in it: the variant, and this
/// interpreter's own text where it carries one.
fn kind<T>(r: &Result<T, Error>) -> String {
match r {
Ok(_) => "Ok".into(),
Err(Error::Malformed { why, .. }) => format!("Malformed: {why}"),
Err(Error::NotFound(_)) => "NotFound".into(),
Err(Error::Exists(_)) => "Exists".into(),
Err(Error::Type(w)) => format!("Type: {w}"),
Err(Error::Rule(w)) => format!("Rule: {w}"),
Err(Error::Table(w)) => format!("Table: {w}"),
Err(Error::Fatal { .. }) => "Fatal".into(),
Err(Error::Bound(w)) => format!("Bound: {w}"),
Err(Error::Unsupported(w)) => format!("Unsupported: {w}"),
Err(Error::Host(_)) => "Host".into(),
}
}
fn shape(v: &Value) -> String {
match v {
Value::Uninitialized => "Uninitialized".into(),
Value::Integer(_) => "Integer".into(),
Value::String(s) => format!("String[{}]", s.len()),
Value::Buffer(b) => format!("Buffer[{}]", b.len()),
Value::Package(p) => format!("Package[{}]", p.len()),
Value::Reference(_) => "Reference".into(),
}
}
// ---- a scan for method definitions, each then checked by evaluating it ----
fn pkg_len(b: &[u8], at: usize) -> Option<(usize, usize)> {
let lead = *b.get(at)?;
let follow = usize::from(lead >> 6);
if follow == 0 {
return Some((usize::from(lead & 0x3F), at + 1));
}
let mut len = usize::from(lead & 0x0F);
for i in 0..follow {
len |= usize::from(*b.get(at + 1 + i)?) << (4 + 8 * i);
}
Some((len, at + 1 + follow))
}
fn seg_ok(s: &[u8]) -> bool {
s.len() == 4
&& matches!(s[0], b'A'..=b'Z' | b'_')
&& s[1..].iter().all(|c| matches!(c, b'A'..=b'Z' | b'0'..=b'9' | b'_'))
}
/// A NameString at `at`, resolved against `scope`: the segments, and where it ends.
fn name_at(b: &[u8], mut at: usize, scope: &[[u8; 4]]) -> Option<(Vec<[u8; 4]>, usize)> {
let mut path: Vec<[u8; 4]> = scope.to_vec();
if *b.get(at)? == b'\\' {
path.clear();
at += 1;
} else {
while *b.get(at)? == b'^' {
path.pop()?;
at += 1;
}
}
let count = match *b.get(at)? {
0x00 => {
at += 1;
0
}
0x2E => {
at += 1;
2
}
0x2F => {
at += 2;
usize::from(*b.get(at - 1)?)
}
_ => 1,
};
for _ in 0..count {
let s = b.get(at..at + 4)?;
if !seg_ok(s) {
return None;
}
path.push(s.try_into().unwrap());
at += 4;
}
Some((path, at))
}
fn text(path: &[[u8; 4]]) -> String {
let segs: Vec<String> = path.iter().map(|s| String::from_utf8_lossy(s).into_owned()).collect();
format!("\\{}", segs.join("."))
}
/// Every method a table seems to define outside any method, as (path, argument count).
fn methods(b: &[u8]) -> Vec<(String, u8)> {
let mut out = Vec::new();
let mut stack: Vec<(usize, Vec<[u8; 4]>)> = vec![(b.len(), Vec::new())];
let mut i = 36;
while i < b.len() {
while stack.len() > 1 && stack.last().unwrap().0 <= i {
stack.pop();
}
let (end, scope) = stack.last().unwrap().clone();
let (op, ext, body) = match b[i] {
0x5B => (b.get(i + 1).copied().unwrap_or(0), true, i + 2),
o => (o, false, i + 1),
};
let skip = |n: usize| i + n;
match (ext, op) {
(false, 0x14) | (false, 0x10) | (true, 0x82..=0x85) => {
let parsed = pkg_len(b, body).and_then(|(len, after)| {
let e = body + len;
(e <= end && e > after).then_some(())?;
let (path, named) = name_at(b, after, &scope)?;
Some((e, path, named))
});
match parsed {
Some((e, path, named)) if !ext && op == 0x14 => {
if let Some(&flags) = b.get(named) {
out.push((text(&path), flags & 7));
}
i = e;
}
Some((e, path, named)) => {
stack.push((e, path));
i = named + if ext && op == 0x83 { 6 } else if ext && op == 0x84 { 3 } else { 0 };
}
None => i += 1,
}
}
// Buffers, packages and field lists hold no definition.
(false, 0x11..=0x13) | (true, 0x81 | 0x86 | 0x87) => match pkg_len(b, body) {
Some((len, _)) if body + len <= end => i = body + len,
_ => i += 1,
},
(false, 0xA0 | 0xA1 | 0xA2) => i = pkg_len(b, body).map_or(i + 1, |(_, after)| after),
(false, 0x0A) => i = skip(2),
(false, 0x0B) => i = skip(3),
(false, 0x0C) => i = skip(5),
(false, 0x0E) => i = skip(9),
(false, 0x0D) => i = b[i..].iter().position(|&c| c == 0).map_or(b.len(), |n| i + n + 1),
(false, 0x08) => i = name_at(b, body, &scope).map_or(i + 1, |(_, after)| after),
_ => i += 1,
}
}
out
}
/// Every string a table passes straight to `_OSI`.
fn osi_strings(b: &[u8]) -> Vec<Vec<u8>> {
let mut out = Vec::new();
for i in 0..b.len().saturating_sub(5) {
if &b[i..i + 4] == b"_OSI" && b[i + 4] == 0x0D {
if let Some(n) = b[i + 5..].iter().position(|&c| c == 0) {
out.push(b[i + 5..i + 5 + n].to_vec());
}
}
}
out
}
fn count_op(b: &[u8], a: u8, c: u8) -> usize {
b.windows(2).filter(|w| w[0] == a && w[1] == c).count()
}
fn tables(dir: &str, show: bool, mem: &BTreeMap<u64, u64>) {
let range = std::env::var("AML_RANGE").ok().map(|s| {
let v: Vec<u64> = s.split(":").map(|x| u64::from_str_radix(x, 16).unwrap()).collect();
(v[0], v[1], v[2])
});
let mut names: Vec<(u32, String)> = Vec::new();
for e in std::fs::read_dir(dir).unwrap() {
let n = e.unwrap().file_name().into_string().unwrap();
if n == "DSDT" || n == "dsdt.bin" {
names.push((0, n));
} else if let Some(k) = n.strip_prefix("SSDT").and_then(|k| k.parse::<u32>().ok()) {
names.push((k, n));
}
}
names.sort();
let mut i = Interpreter::new();
let base = live();
let (mut bytes_total, mut loaded, mut refused) = (0usize, 0, 0);
let mut all: Vec<Vec<u8>> = Vec::new();
for (_, n) in &names {
let t = std::fs::read(format!("{dir}/{n}")).unwrap();
let mut h = Count { mem: mem.clone(), range, ..Count::default() };
let before = live();
let r = open_load(&mut i, &mut h, &t);
if r.is_ok() {
loaded += 1;
bytes_total += t.len();
} else {
refused += 1;
}
println!(
"load {} len {} -> {} | heap +{} | {} | bytes 5B20 {} 5B1F {} 5B88 {}",
if show { n.as_str() } else { &n[..4] },
t.len(),
kind(&r),
live().wrapping_sub(before) as isize,
h.line(),
count_op(&t, 0x5B, 0x20),
count_op(&t, 0x5B, 0x1F),
count_op(&t, 0x5B, 0x88),
);
if show {
if let Err(e) = &r {
println!(" {e:?}");
}
}
all.push(t);
}
println!("tables {} loaded {} refused {} | loaded bytes {} | heap held by the interpreter {}", names.len(), loaded, refused, bytes_total, live() - base);
{
let d = &all[0];
let t = toyos_acpi::Table::open(Image(d), 0, b"DSDT", 0).unwrap();
let scanned = toyos_acpi::s5_slp_typ(&t);
let ours = i.evaluate(&mut Count::default(), "\\_S5", &[]);
let first = match &ours { Ok(Value::Package(p)) => p.first().cloned(), _ => None };
let agree = matches!((&first, scanned), (Some(Value::Integer(a)), toyos_acpi::S5::SlpTyp(b)) if *a == u64::from(b));
println!("differential: the first element of \\_S5 against toyos_acpi::s5_slp_typ of the same DSDT -> {}", if agree { "equal" } else { "DIFFERENT" });
}
let mut h = Count { mem: mem.clone(), range, ..Count::default() };
for s in ["\\_S0", "\\_S3", "\\_S4", "\\_S5"] {
let r = i.evaluate(&mut h, s, &[]);
match (&r, show) {
(Ok(v), true) => println!("evaluate {s} -> {v:?}"),
(Ok(v), false) => println!("evaluate {s} -> Ok {}", shape(v)),
_ => println!("evaluate {s} -> {}", kind(&r)),
}
}
println!(" during those: {}", h.line());
let mut asked: Vec<Vec<u8>> = all.iter().flat_map(|t| osi_strings(t)).collect();
asked.sort();
asked.dedup();
let (mut yes, mut no, mut no_windows) = (0, 0, 0);
for s in &asked {
let r = i.evaluate(&mut h, "\\_OSI", &[Value::String(s.clone())]);
let said = matches!(r, Ok(Value::Integer(v)) if v != 0);
if said { yes += 1 } else { no += 1 }
if !said && s.starts_with(b"Windows") {
no_windows += 1;
}
if show {
println!(" _OSI({:?}) -> {}", String::from_utf8_lossy(s), said);
}
}
println!("_OSI: {} distinct strings asked; {} answered yes, {} answered no ({} of those begin \"Windows\")", asked.len(), yes, no, no_windows);
let mut found: Vec<(String, u8)> = all.iter().flat_map(|t| methods(t)).collect();
found.sort();
found.dedup();
let zero: Vec<&String> = found.iter().filter(|(_, a)| *a == 0).map(|(p, _)| p).collect();
let mut tally: BTreeMap<String, usize> = BTreeMap::new();
let mut h = Count { mem: mem.clone(), range, ..Count::default() };
let before = live();
let start = std::time::Instant::now();
let mut slowest = std::time::Duration::ZERO;
for p in &zero {
let t = std::time::Instant::now();
let r = i.evaluate(&mut h, p, &[]);
slowest = slowest.max(t.elapsed());
let k = match &r {
Ok(v) => format!("Ok {}", shape(v).split('[').next().unwrap()),
Err(Error::NotFound(n)) if n == *p => "NotFound: the scanned path itself (no such object, or the scan misread)".into(),
Err(Error::NotFound(_)) => "NotFound: a name the method uses".into(),
_ => kind(&r),
};
if show {
println!(" {p} -> {k} {}", if let Ok(v) = &r { format!("{v:x?}") } else { String::new() });
}
*tally.entry(k).or_default() += 1;
}
println!("methods: {} definitions scanned, {} of them take no argument; evaluated each once in {:?} (slowest {:?}):", found.len(), zero.len(), start.elapsed(), slowest);
for (k, n) in &tally {
println!(" {n:5} {k}");
}
println!(" during those: {}", h.line());
println!(" heap after the sweep {:+} against before it; process peak {}", live() as isize - before as isize, PEAK.load(Relaxed));
let r = i.evaluate(&mut h, "\\_S5", &[]);
println!("evaluate \\_S5 after the sweep -> {}", kind(&r));
}
fn report(what: &str, r: Result<Value, Error>) {
match &r {
Ok(v) => println!("{what} -> Ok {}", shape(v)),
Err(e) => println!("{what} -> {e:?}"),
}
}
const ZERO: &[u8] = &[0x00];
/// Review round 2, first BLOCKER: `Return (RefOf (Local0))`, stored through.
fn chain() {
let m = method("M", 0, &ret(&ref_of(&local(0))));
let main = method(
"MAIN",
0,
&while_(
&int(1),
&cat(&[&store(&name("M"), &local(2)), &store(&local(0), &deref(&local(2))), &store(&local(2), &local(0))]),
),
);
let (mut i, mut h) = loaded(&cat(&[&m, &main]));
report("chain", i.evaluate(&mut h, "\\MAIN", &[]));
}
fn selfref() {
let m = method("M", 0, &ret(&ref_of(&local(0))));
let body = cat(&[
&store(&int(0), &local(3)),
&while_(
&lless(&local(3), &int(50_000)),
&cat(&[&store(&name("M"), &local(2)), &store(&local(2), &deref(&local(2))), &increment(&local(3))]),
),
]);
let (mut i, mut h) = loaded(&cat(&[&m, &method("MAIN", 0, &body)]));
let before = live();
report("selfref", i.evaluate(&mut h, "\\MAIN", &[]));
println!("selfref: heap {:+} bytes after one evaluation", live() as isize - before as isize);
let before = live();
report("selfref", i.evaluate(&mut h, "\\MAIN", &[]));
println!("selfref: heap {:+} bytes after a second", live() as isize - before as isize);
}
/// A LocalX holding a reference to itself and two holding each other, in a
/// frame that is alive, fifty thousand times over.
fn liveref() {
let body = cat(&[
&store(&ref_of(&local(1)), &local(2)),
&store(&local(2), &deref(&local(2))),
&store(&ref_of(&local(4)), &local(5)),
&store(&ref_of(&local(5)), &local(4)),
]);
let main = cat(&[
&store(&int(0), &local(3)),
&while_(&lless(&local(3), &int(50_000)), &cat(&[&name("CYC"), &increment(&local(3))])),
]);
let (mut i, mut h) = loaded(&cat(&[&method("CYC", 0, &body), &method("MAIN", 0, &main)]));
for round in 0..2 {
let before = live();
report("liveref", i.evaluate(&mut h, "\\MAIN", &[]));
println!("liveref: heap {:+} bytes after evaluation {round}", live() as isize - before as isize);
}
}
/// The ArgX path: a callee stores a reference to its own LocalX through an
/// ArgX that refers to the caller's.
fn argref() {
let callee = method("PUT", 1, &store(&ref_of(&local(1)), &arg(0)));
let main = method(
"MAIN",
0,
&while_(
&int(1),
&cat(&[
&cat(&[&name("PUT"), &ref_of(&local(2))]),
&store(&local(0), &deref(&local(2))),
&store(&local(2), &local(0)),
]),
),
);
let (mut i, mut h) = loaded(&cat(&[&callee, &main]));
report("argref", i.evaluate(&mut h, "\\MAIN", &[]));
}
/// Second BLOCKER: a String of 98,303 characters stored to a field of 1 MiB.
fn strfield(cap: usize) {
let body = cat(&[
&op_region("MEM", 0x00, &int(0), &int(0x10_0000)),
&field("MEM", 0x01, &[unit("HUGE", 0x80_0000)]),
&method("MAIN", 0, &store(&op1(0x98, &buffer(&int(0x8000), &[]), ZERO), &name("HUGE"))),
]);
let mut h = Count::default();
let mut i = Interpreter::new();
open_load(&mut i, &mut h, &dsdt(&body)).unwrap();
PEAK.store(live(), Relaxed);
CAP.store(live() + cap, Relaxed);
let r = i.evaluate(&mut h, "\\MAIN", &[]);
CAP.store(usize::MAX, Relaxed);
println!("strfield -> {} | peak heap during it {} | {}", kind(&r), PEAK.load(Relaxed), h.line());
}
/// The outside review's third finding, measured: what a load leaves held
/// that is no string, buffer or package.
fn retained(k: usize) {
let mut h = Count::default();
let mut i = Interpreter::new();
let base = live();
open_load(&mut i, &mut h, &dsdt(&[])).unwrap();
// One method a table, the rest of its mebibyte Noops in that method.
let mut held = 0usize;
for n in 0..k {
let nm = format!("M{n:03}");
let pad = vec![0xA3u8; (1 << 20) - 36 - 16];
let t = table(b"SSDT", 2, &method(&nm, 0, &pad));
let r = open_load(&mut i, &mut h, &t);
if r.is_ok() {
held += t.len();
}
println!("retained: table {n} of {} bytes -> {} | heap held {}", t.len(), kind(&r), live() - base);
if r.is_err() {
break;
}
}
println!("retained: {held} table bytes loaded, heap held {}", live() - base);
}
/// The densest namespace a table can ask for: field units, five bytes each.
fn dense() {
let mut units = Vec::new();
let alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let mut n = 0usize;
// Field lists of 4000 units each, every one under a device of its own so
// that names repeat.
let mut body = op_region("MEM", 0x00, &int(0), &int(0x10_0000));
let mut dev = 0usize;
while body.len() < (1 << 20) - 36 - 30_000 {
units.clear();
for u in 0..4000usize {
let s = [b'A' + (u / (36 * 36)) as u8, alphabet[(u / 36) % 36], alphabet[u % 36], b'_'];
units.push(cat(&[&s, &[0x08]]));
n += 1;
}
let d = format!("D{:03}", dev);
dev += 1;
body.extend(device(&d, &field("\\MEM", 0x01, &units)));
}
let t = dsdt(&body);
let mut h = Count::default();
let mut i = Interpreter::new();
let base = live();
let r = open_load(&mut i, &mut h, &t);
println!("dense: a table of {} bytes naming {} field units -> {} | heap held {}", t.len(), n, kind(&r), live() - base);
}
fn main() {
let a: Vec<String> = std::env::args().collect();
let show = a.iter().any(|x| x == "--show");
match a[1].as_str() {
"tables" => {
let mem = a.iter().filter_map(|x| x.strip_prefix("--mem=")).map(|kv| {
let (k, v) = kv.split_once(":").unwrap();
(u64::from_str_radix(k, 16).unwrap(), u64::from_str_radix(v, 16).unwrap())
}).collect();
tables(&a[2], show, &mem)
}
"chain" => chain(),
"selfref" => selfref(),
"liveref" => liveref(),
"argref" => argref(),
"strfield" => strfield(a[2].parse::<usize>().unwrap() << 20),
"retained" => retained(a[2].parse().unwrap()),
"dense" => dense(),
"fill-names" => fill_names(),
"fill-lazy" => fill_lazy(),
"fill-buffers" => fill_buffers(),
_ => panic!("what?"),
}
}
/// Loads tables made by `make` until one is refused, and says what the
/// process held at the last one that loaded and at its peak.
pub fn until_refused(what: &str, first: &[u8], make: impl Fn(usize) -> Vec<u8>) {
let mut h = Count::default();
let mut i = Interpreter::new();
let base = live();
open_load(&mut i, &mut h, &dsdt(first)).unwrap();
PEAK.store(live(), Relaxed);
let mut held = live() - base;
for n in 0..400 {
let t = make(n);
let r = open_load(&mut i, &mut h, &t);
drop(t);
if r.is_err() {
println!("{what}: table {n} -> {} | heap held at the last that loaded {held} | peak {} | held after the refusal {}", kind(&r), PEAK.load(Relaxed) - base, live() - base);
return;
}
held = live() - base;
}
println!("{what}: 400 tables loaded, heap held {held}");
}
pub fn fill_names() {
let digits = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let units: Vec<Vec<u8>> = (0..4000usize)
.map(|u| unit(std::str::from_utf8(&[b'A' + (u / 1296) as u8, digits[u / 36 % 36], digits[u % 36]]).unwrap(), 8))
.collect();
// Fifty-one devices a table, then fewer, so the last table that loads ends near the bound.
until_refused("names", &op_region("MEM", 0x00, &int(0), &int(0x1000)), |t| {
let per = if t == 0 { 40 } else { 1 };
let devices: Vec<Vec<u8>> = (0..per).map(|d| device(&format!("D{:01}{d:02}", digits[t % 36] as char), &field("\\MEM", 0x01, &units))).collect();
table(b"SSDT", 2, &devices.concat())
});
}
pub fn fill_lazy() {
// Packages of 255 names that resolve to nothing yet, four bytes each.
until_refused("unresolved names in packages", &[], |t| {
let digits = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let per = if t == 0 { 600 } else { 20 };
let elems: Vec<Vec<u8>> = (0..255).map(|_| b"ZZZZ".to_vec()).collect();
let body: Vec<u8> = (0..per)
.flat_map(|p: usize| def_name(&format!("P{}{}", digits[p / 36 % 36] as char, digits[p % 36] as char), &package(&elems)))
.collect();
table(b"SSDT", 2, &device(&format!("L{t:03}"), &body))
});
}
pub fn fill_buffers() {
until_refused("buffers", &[], |t| {
let digits = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let per = if t < 15 { 16 } else { 1 };
let body: Vec<u8> = (0..per).flat_map(|p: usize| def_name(&format!("B{}{}", digits[t % 36] as char, digits[p % 36] as char), &buffer(&int(0x1_0000), &[]))).collect();
table(b"SSDT", 2, &body)
});
} |
|
Review of #739 at Net: 17 files, +5587/−3. Production: +3323 ( Read, not run. Every finding below that says "by reading" has no measurement behind it yet; the implementer's run decides it. Round 2 BLOCKERs
Round 1 BLOCKERs still open at round 2
PrivacyPasses. The local tables were unpacked into scratch and removed again. Their header OEM IDs, OEM table IDs and creator IDs, and every string of seven characters or more in them, were searched for in the diff, every commit message, the body and all six comments: the only hits are Microsoft's published BLOCKER
NOTE
SEND BACK |
…dge names no bus Review round 3 of #739. Each finding was measured at 488a05a before it was changed, by the out-of-tree harness and by the new tests run against the old source. A store to a field held a borrow of its source across the write, and a write to a PCI_Config field runs firmware's _ADR, _BBN and _SEG: one that stores to the source panicked, "RefCell already borrowed". The store now writes from a copy of its own, charged and held against the meter, so it is of what the source held when it began. A package element naming an object not yet defined kept its Path outside the meter. Filled with 255-segment names the heap behind a full meter was 713,799,584 bytes. The element is now an Unresolved, metered at its own size and its segments': the same fill holds 15,953,360, and one-segment names 19,549,520 where they held 63,533,264. The bus below a bridge was whatever byte its Secondary Bus Number register answered, kept for the region's life. A register that answers a bus not above the bridge's own, as an unconfigured bridge's 0 does, now names no bus and is refused; and nothing is kept: every access asks the bridges and firmware's methods again, so a bridge renumbered since is seen. A region declared in a method addresses the device the method is in: the walk counts devices alone. A name cost one step however long it was and however far it was searched for. Every namespace walk now pays a step for each scope climbed and each segment looked up (resolve, create, path_of), and a NameString is charged for its bytes. A lone name that is nowhere, looked for from 5,000 scopes down in a loop, ran 11.9 s before the step bound and now runs 3.4 ms. The same defect on the paths it left, found by reading every operator for work a table sizes: a long buffer stored to a short one or to a buffer field, ToString and Mid of a long buffer's first byte, ToInteger and DerefOf of a long string. Each is charged for what it reads. ToInteger of a 64 KiB string of zeros in a loop ran 49 s and now runs 73 ms. _OSI copied its argument to compare it and no longer does. The tests name nothing that the local T14 tables hold and main did not, beyond what the specification itself names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Round 4 negative control and mutations, at In a first run, on the commit this head amends, The script's log: The patches,
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 9d1f5f4f8..1f078f767 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -205,7 +205,7 @@ impl Machine<'_> {
let mut bus = u8::try_from(bus).map_err(|_| Error::Rule("a _BBN above 0xFF (§6.5.5)"))?;
let segment = u16::try_from(segment).map_err(|_| Error::Rule("a _SEG above 0xFFFF (§6.5.6)"))?;
let Some((&device, bridges)) = below.split_first() else { return self.function(host, segment, bus) };
- for &bridge in bridges.iter().rev() {
+ for &bridge in bridges.iter() {
let b = self.function(bridge, segment, bus)?;
let secondary = Address::PciConfig { segment, bus, device: b.device, function: b.function, offset: 0x19 };
let answered = self.host.read(secondary, crate::Access::Byte).map_err(|d| Error::Host(d.0))? as u8;
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 9d1f5f4f8..b789161f0 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -212,7 +212,7 @@ impl Machine<'_> {
// The register resets to 0, and a configured bridge's secondary
// bus is above the bus the bridge is on: any other answer would
// address a device that is not below this bridge.
- if answered <= bus {
+ if false {
return Err(Error::Rule("a bridge's Secondary Bus Number is not above its own bus, and names no bus below it"));
}
bus = answered;
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 9d1f5f4f8..db00963f4 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -212,7 +212,7 @@ impl Machine<'_> {
// The register resets to 0, and a configured bridge's secondary
// bus is above the bus the bridge is on: any other answer would
// address a device that is not below this bridge.
- if answered <= bus {
+ if answered < bus {
return Err(Error::Rule("a bridge's Secondary Bus Number is not above its own bus, and names no bus below it"));
}
bus = answered;
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 9d1f5f4f8..8b0527b95 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -188,7 +188,7 @@ impl Machine<'_> {
let mut host = r.scope;
loop {
self.step()?;
- if matches!(self.ns.object(host), Some(Object::Device)) {
+ if true {
if self.ns.child(host, bbn).is_some() {
break;
}
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 9d1f5f4f8..43835655e 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -207,7 +207,7 @@ impl Machine<'_> {
let Some((&device, bridges)) = below.split_first() else { return self.function(host, segment, bus) };
for &bridge in bridges.iter().rev() {
let b = self.function(bridge, segment, bus)?;
- let secondary = Address::PciConfig { segment, bus, device: b.device, function: b.function, offset: 0x19 };
+ let secondary = Address::PciConfig { segment, bus, device: b.device, function: b.function, offset: 0x18 };
let answered = self.host.read(secondary, crate::Access::Byte).map_err(|d| Error::Host(d.0))? as u8;
// The register resets to 0, and a configured bridge's secondary
// bus is above the bus the bridge is on: any other answer would
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 9d1f5f4f8..61808e7f9 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -311,8 +311,7 @@ impl Machine<'_> {
(&int, int.len())
}
Object::Buf(b) | Object::Str(b) => {
- let bytes = b.borrow().clone();
- copy = self.bytes(bytes)?;
+ copy = b.clone();
held = copy.borrow();
match &v {
Object::Str(_) => (&held, 1),
diff --git a/userland/acpiserver/aml/src/object.rs b/userland/acpiserver/aml/src/object.rs
index c7beaabaa..5cb7787dd 100644
--- a/userland/acpiserver/aml/src/object.rs
+++ b/userland/acpiserver/aml/src/object.rs
@@ -156,7 +156,8 @@ pub(crate) struct Unresolved {
impl Unresolved {
fn held(path: &Path) -> usize {
- core::mem::size_of::<Unresolved>() + core::mem::size_of_val(path.segs.as_slice())
+ let _ = path;
+ 0
}
}
diff --git a/userland/acpiserver/aml/src/object.rs b/userland/acpiserver/aml/src/object.rs
index c7beaabaa..b6398a8f7 100644
--- a/userland/acpiserver/aml/src/object.rs
+++ b/userland/acpiserver/aml/src/object.rs
@@ -156,7 +156,8 @@ pub(crate) struct Unresolved {
impl Unresolved {
fn held(path: &Path) -> usize {
- core::mem::size_of::<Unresolved>() + core::mem::size_of_val(path.segs.as_slice())
+ let _ = path;
+ core::mem::size_of::<Unresolved>()
}
}
diff --git a/userland/acpiserver/aml/src/namespace.rs b/userland/acpiserver/aml/src/namespace.rs
index d749ec868..ba28717b0 100644
--- a/userland/acpiserver/aml/src/namespace.rs
+++ b/userland/acpiserver/aml/src/namespace.rs
@@ -113,7 +113,6 @@ impl Namespace {
let Some(mut at) = self.start(scope, path, toll)? else { return Ok(None) };
if path.searches() {
loop {
- toll()?;
if let Some(found) = self.child(at, path.segs[0]) {
return Ok(Some(found));
}
diff --git a/userland/acpiserver/aml/src/namespace.rs b/userland/acpiserver/aml/src/namespace.rs
index d749ec868..f58b28576 100644
--- a/userland/acpiserver/aml/src/namespace.rs
+++ b/userland/acpiserver/aml/src/namespace.rs
@@ -122,7 +122,6 @@ impl Namespace {
}
}
for &seg in &path.segs {
- toll()?;
let Some(below) = self.child(at, seg) else { return Ok(None) };
at = below;
}
diff --git a/userland/acpiserver/aml/src/namespace.rs b/userland/acpiserver/aml/src/namespace.rs
index d749ec868..9ac937c16 100644
--- a/userland/acpiserver/aml/src/namespace.rs
+++ b/userland/acpiserver/aml/src/namespace.rs
@@ -98,8 +98,8 @@ impl Namespace {
return Ok(Some(self.root()));
}
let mut at = scope;
+ let _ = &toll;
for _ in 0..path.up {
- toll()?;
let Some(above) = self.parent(at) else { return Ok(None) };
at = above;
}
diff --git a/userland/acpiserver/aml/src/namespace.rs b/userland/acpiserver/aml/src/namespace.rs
index d749ec868..03a205834 100644
--- a/userland/acpiserver/aml/src/namespace.rs
+++ b/userland/acpiserver/aml/src/namespace.rs
@@ -137,7 +137,6 @@ impl Namespace {
let missing = || Error::NotFound(crate::name::text(path));
let mut at = self.start(scope, path, toll)?.ok_or_else(missing)?;
for &seg in parents {
- toll()?;
at = self.child(at, seg).ok_or_else(missing)?;
}
if self.node(at).is_some_and(|n| n.children.contains_key(last)) {
diff --git a/userland/acpiserver/aml/src/namespace.rs b/userland/acpiserver/aml/src/namespace.rs
index d749ec868..1ee27a1bf 100644
--- a/userland/acpiserver/aml/src/namespace.rs
+++ b/userland/acpiserver/aml/src/namespace.rs
@@ -211,7 +211,7 @@ impl Namespace {
let mut at = id;
while let Some(n) = self.node(at) {
let Some(p) = n.parent else { break };
- toll()?;
+ let _ = &toll;
segs.push(n.seg);
at = p;
}
diff --git a/userland/acpiserver/aml/src/exec.rs b/userland/acpiserver/aml/src/exec.rs
index 0aa14d718..907b70f40 100644
--- a/userland/acpiserver/aml/src/exec.rs
+++ b/userland/acpiserver/aml/src/exec.rs
@@ -193,7 +193,7 @@ impl<'a> Machine<'a> {
fn name(&mut self, c: &mut Cursor<'_>) -> Result<Path, Error> {
let at = c.at;
let p = c.name()?;
- self.charge(c.at - at)?;
+ let _ = at;
Ok(p)
}
diff --git a/userland/acpiserver/aml/src/exec.rs b/userland/acpiserver/aml/src/exec.rs
index 0aa14d718..ed119f424 100644
--- a/userland/acpiserver/aml/src/exec.rs
+++ b/userland/acpiserver/aml/src/exec.rs
@@ -1206,7 +1206,7 @@ impl<'a> Machine<'a> {
// Table 19.7: a buffer that exists keeps its size.
let n = to_buf(&v, w)?;
let len = b.borrow().len();
- self.charge(n.len().max(len))?;
+ self.charge(len)?;
b.replace(fit(n, len))
}
Object::Pkg(p) => match &v {
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 9d1f5f4f8..d5da6177d 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -392,7 +392,7 @@ impl Machine<'_> {
_ => return Err(Error::Type("a store to a buffer field of an object that is not an integer, buffer or string")),
};
// The source is read whole, and the field written a bit at a time.
- self.charge(src.len().saturating_add(usize::try_from(f.len).unwrap_or(usize::MAX)))?;
+ self.charge(usize::try_from(f.len).unwrap_or(usize::MAX))?;
let src = fit(src, bytes_for(f.len)?);
let mut d = f.data.bits();
if f.bit.saturating_add(f.len) > (d.len() as u64).saturating_mul(8) {
diff --git a/userland/acpiserver/aml/src/exec.rs b/userland/acpiserver/aml/src/exec.rs
index 0aa14d718..a1f740572 100644
--- a/userland/acpiserver/aml/src/exec.rs
+++ b/userland/acpiserver/aml/src/exec.rs
@@ -1703,7 +1703,6 @@ impl<'a> Machine<'a> {
}),
0x9C => {
let b = to_buf(&src, w)?;
- self.charge(b.len())?;
let n = self.int_arg(f, c)?;
let n = if n == w.ones() { usize::MAX } else { usize::try_from(n).unwrap_or(usize::MAX) };
self.new_str(b.iter().take(n).take_while(|&&x| x != 0).copied().collect())?
diff --git a/userland/acpiserver/aml/src/exec.rs b/userland/acpiserver/aml/src/exec.rs
index 0aa14d718..86ad82402 100644
--- a/userland/acpiserver/aml/src/exec.rs
+++ b/userland/acpiserver/aml/src/exec.rs
@@ -1715,7 +1715,6 @@ impl<'a> Machine<'a> {
Object::Str(s) => (s.borrow().clone(), true),
o => (to_buf(o, w)?, false),
};
- self.charge(data.len())?;
let start = usize::try_from(i).unwrap_or(usize::MAX).min(data.len());
let end = start.saturating_add(usize::try_from(n).unwrap_or(usize::MAX)).min(data.len());
let part = data[start..end].to_vec();
diff --git a/userland/acpiserver/aml/src/exec.rs b/userland/acpiserver/aml/src/exec.rs
index 0aa14d718..bc009be54 100644
--- a/userland/acpiserver/aml/src/exec.rs
+++ b/userland/acpiserver/aml/src/exec.rs
@@ -1696,7 +1696,6 @@ impl<'a> Machine<'a> {
},
0x99 => Object::Int(match &src {
Object::Str(s) => {
- self.charge(s.borrow().len())?;
int_of_text(&s.borrow(), w)?
}
o => to_int(o, w)?,
diff --git a/userland/acpiserver/aml/src/exec.rs b/userland/acpiserver/aml/src/exec.rs
index 0aa14d718..129d11142 100644
--- a/userland/acpiserver/aml/src/exec.rs
+++ b/userland/acpiserver/aml/src/exec.rs
@@ -211,7 +211,6 @@ impl<'a> Machine<'a> {
/// DerefOf of a String names an object by ASL text (§19.6.30), read whole.
fn path_of_text(&mut self, s: &Bytes) -> Result<Path, Error> {
- self.charge(s.borrow().len())?;
Path::text(&s.borrow()).ok_or(Error::Rule("DerefOf of a String that is not a name (§19.6.30)"))
}
|
|
The out-of-tree check behind the body's round 4 numbers. It is round 3's check (its source is in the comment above that begins "The out-of-tree check behind the body's T14 and heap numbers") with the changes below, built with Commands, each
The host was shared and loaded while these ran: load averages near 100 during the "before" runs and near 35 during the "after" ones. The times in the body are single runs under that load. Changes to round 3's source, as 61a62,64
> /// Whether a bridge answers its Secondary Bus Number register as one
> /// configured would: the bus after its own.
> bridges: bool,
80a84
> Address::PciConfig { bus, offset: 0x19, .. } if self.bridges && w == Access::Byte => u64::from(bus) + 1,
289c293
< fn tables(dir: &str, show: bool, mem: &BTreeMap<u64, u64>) {
---
> fn tables(dir: &str, show: bool, mem: &BTreeMap<u64, u64>, bridges: bool) {
310c314
< let mut h = Count { mem: mem.clone(), range, ..Count::default() };
---
> let mut h = Count { mem: mem.clone(), range, bridges, ..Count::default() };
348c352
< let mut h = Count { mem: mem.clone(), range, ..Count::default() };
---
> let mut h = Count { mem: mem.clone(), range, bridges, ..Count::default() };
381c385
< let mut h = Count { mem: mem.clone(), range, ..Count::default() };
---
> let mut h = Count { mem: mem.clone(), range, bridges, ..Count::default() };
396c400
< println!(" {p} -> {k} {}", if let Ok(v) = &r { format!("{v:x?}") } else { String::new() });
---
> println!(" {p} -> {k} {}", match &r { Ok(v) => format!("{v:x?}"), Err(e) => format!("{e:?}") });
565a570,572
> if round4(&a) {
> return;
> }
572c579
< tables(&a[2], show, &mem)
---
> tables(&a[2], show, &mem, a.iter().any(|x| x == "--bridges"))
597c604
< for n in 0..400 {
---
> for n in 0..4000 {Appended to it: // ---- round 4 -------------------------------------------------------------
/// Packages of names that resolve to nothing, each name 255 segments long.
pub fn fill_lazy_long() {
let long = cat(&[&[b'\\', 0x2F, 255], &b"ZZZZ".repeat(255)]);
until_refused("unresolved 255-segment names in packages", &[], |t| {
let elems: Vec<Vec<u8>> = (0..255).map(|_| long.clone()).collect();
let body: Vec<u8> = (0..4).flat_map(|p: usize| def_name(&format!("P{p}"), &package(&elems))).collect();
table(b"SSDT", 2, &device(&format!("L{t:03}"), &body))
});
}
/// `While (One) { <op> Increment (\CNT) }` in `\MAIN` at the bottom of `outer`
/// nested devices, after `setup` at the root: how far it got, and how long it took.
fn looped(what: &str, i: &mut Interpreter, h: &mut Count, main: &str) {
let start = std::time::Instant::now();
let r = i.evaluate(h, main, &[]);
let took = start.elapsed();
let cnt = i.evaluate(h, "\\CNT", &[]);
println!("{what} -> {} | iterations {:?} | {:?}", kind(&r), cnt, took);
}
fn spin(op: &[u8]) -> Vec<u8> {
method("MAIN", 0, &while_(&int(1), &cat(&[op, &increment(&name("\\CNT"))])))
}
fn cond_ref_of(n: &[u8]) -> Vec<u8> {
cat(&[&[0x5B, 0x12], n, &local(0)])
}
/// `depth` devices nested one in the next, `inner` in the last.
fn nested(depth: usize, inner: &[u8]) -> Vec<u8> {
let mut b = inner.to_vec();
for d in (0..depth).rev() {
b = device(&format!("N{d:03}"), &b);
}
b
}
fn nested_path(depth: usize) -> String {
let segs: Vec<String> = (0..depth).map(|d| format!("N{d:03}")).collect();
format!("\\{}", segs.join("."))
}
/// A lone NameSeg that names nothing, looked for from `depth` scopes down.
fn miss(depth: usize) {
let (mut i, mut h) = (Interpreter::new(), Count::default());
let body = cat(&[&def_name("CNT", &int(0)), &nested(depth, &spin(&cond_ref_of(b"ZZZZ")))]);
open_load(&mut i, &mut h, &dsdt(&body)).unwrap();
looped(&format!("miss from {depth} scopes down"), &mut i, &mut h, &format!("{}.MAIN", nested_path(depth)));
}
/// The same from a scope `tables * 200` deep, each table opening the last one's
/// deepest device through an alias at the root.
fn miss_chain(tables: usize) {
let (mut i, mut h) = (Interpreter::new(), Count::default());
open_load(&mut i, &mut h, &dsdt(&cat(&[&def_name("CNT", &int(0)), &device("A000", &[])]))).unwrap();
for t in 0..tables {
let last = t + 1 == tables;
let tail = cat(&[&[0x06], &name("N199"), &name(&format!("\\A{:03}", t + 1))]);
let inner = device("N199", &if last { spin(&cond_ref_of(b"ZZZZ")) } else { tail });
let r = open_load(&mut i, &mut h, &table(b"SSDT", 2, &scope(&format!("\\A{t:03}"), &nested(199, &inner))));
if r.is_err() {
println!("miss-chain: table {t} -> {}", kind(&r));
return;
}
}
let path = format!("\\A{:03}.{}.N199.MAIN", tables - 1, &nested_path(199)[1..]);
looped(&format!("miss from {} scopes down", tables * 200), &mut i, &mut h, &path);
}
/// A name behind `n` parent prefixes.
fn carets(n: usize) {
let (mut i, mut h) = (Interpreter::new(), Count::default());
let nm = cat(&[&vec![b'^'; n], b"ZZZZ"]);
open_load(&mut i, &mut h, &dsdt(&cat(&[&def_name("CNT", &int(0)), &spin(&cond_ref_of(&nm))]))).unwrap();
looped(&format!("a name behind {n} parent prefixes"), &mut i, &mut h, "\\MAIN");
}
/// A path of `depth` segments that resolves.
fn segs(depth: usize) {
let (mut i, mut h) = (Interpreter::new(), Count::default());
let body = cat(&[&def_name("CNT", &int(0)), &nested(depth, &[]), &spin(&cond_ref_of(&name(&nested_path(depth))))]);
open_load(&mut i, &mut h, &dsdt(&body)).unwrap();
looped(&format!("a path of {depth} segments"), &mut i, &mut h, "\\MAIN");
}
/// A Notify of a device `depth` scopes down.
fn notify_deep(depth: usize) {
let (mut i, mut h) = (Interpreter::new(), Count::default());
let op = cat(&[&[0x86], &name("^"), &int(0x80)]);
let body = cat(&[&def_name("CNT", &int(0)), &nested(depth, &spin(&op))]);
open_load(&mut i, &mut h, &dsdt(&body)).unwrap();
looped(&format!("Notify of a device {depth} scopes down"), &mut i, &mut h, &format!("{}.MAIN", nested_path(depth)));
}
/// An operator that walks or copies an object of `size` bytes for a result of a few.
fn sized(case: &str, size: usize) {
let big = buffer(&int(size as u64), &[]);
let (setup, op): (Vec<u8>, Vec<u8>) = match case {
// _OSI of a long string.
"osi" => (def_name("BIG", &whole_string(&vec![b'A'; size])), cat(&[&name("\\_OSI"), &name("BIG")])),
// A long buffer stored to a named buffer of one byte.
"bufstore" => (cat(&[&def_name("BIG", &big), &def_name("SMAL", &buffer(&int(1), &[]))]), store(&name("BIG"), &name("SMAL"))),
// The same to a one-bit buffer field.
"bitstore" => (
cat(&[&def_name("BIG", &big), &def_name("SMAL", &buffer(&int(1), &[])), &cat(&[&[0x8D], &name("SMAL"), &int(0), &name("BIT0")])]),
store(&name("BIG"), &name("BIT0")),
),
// ToString of one character of it.
"tostring" => (def_name("BIG", &buffer(&int(size as u64), &vec![b'A'; size])), cat(&[&[0x9C], &name("BIG"), &int(1), &local(0)])),
// Mid of one byte of it.
"mid" => (def_name("BIG", &big), cat(&[&[0x9E], &name("BIG"), &int(0), &int(1), &local(0)])),
// ToInteger of a string of zeros.
"toint" => (def_name("BIG", &whole_string(&vec![b'0'; size])), cat(&[&[0x99], &name("BIG"), &local(0)])),
// CondRefOf (DerefOf (a string that is no name)) refuses; a name of many segments does not.
"derefstr" => {
let text: Vec<u8> = std::iter::once(b'\\').chain(std::iter::repeat_n(b'^', size)).collect();
(def_name("BIG", &whole_string(&text)), store(&deref(&name("BIG")), &local(0)))
}
_ => panic!("what?"),
};
let (mut i, mut h) = (Interpreter::new(), Count::default());
// Name takes a DataObject alone, so a string made by an operator is made at load by a method.
let body = match case {
"osi" | "toint" | "derefstr" => {
let made = &setup[1 + 4..];
cat(&[&def_name("CNT", &int(0)), &def_name("BIG", &string("")), &store(made, &name("BIG")), &spin(&op)])
}
_ => cat(&[&def_name("CNT", &int(0)), &setup, &spin(&op)]),
};
let r = open_load(&mut i, &mut h, &dsdt(&body));
if r.is_err() {
println!("{case}: load -> {}", kind(&r));
return;
}
looped(&format!("{case} over {size} bytes"), &mut i, &mut h, "\\MAIN");
}
/// Review round 3, first BLOCKER: a field store whose `_ADR` stores to the source.
fn fieldborrow(string: bool) {
let src = if string { string_of("ABCD") } else { buffer(&int(4), &[]) };
let body = device(
"PCI0",
&cat(&[
&def_name("_BBN", &int(0)),
&def_name("BUFF", &src),
&method("_ADR", 0, &cat(&[&store(&int(0), &name("BUFF")), &ret(&int(0))])),
&op_region("CFG", 0x02, &int(0), &int(0x10)),
&field("CFG", 0x01, &[unit("FLD", 32)]),
&method("MAIN", 0, &store(&name("BUFF"), &name("FLD"))),
]),
);
let (mut i, mut h) = (Interpreter::new(), Count::default());
open_load(&mut i, &mut h, &dsdt(&body)).unwrap();
report("fieldborrow", i.evaluate(&mut h, "\\PCI0.MAIN", &[]));
}
fn whole_string(b: &[u8]) -> Vec<u8> {
cat(&[&[0x9C], &buffer(&int(b.len() as u64), b), &ones(), ZERO])
}
fn string_of(s: &str) -> Vec<u8> {
string(s)
}
pub fn round4(a: &[String]) -> bool {
let n = || a[2].parse::<usize>().unwrap();
match a[1].as_str() {
"fill-lazy-long" => fill_lazy_long(),
"miss" => miss(n()),
"miss-chain" => miss_chain(n()),
"carets" => carets(n()),
"segs" => segs(n()),
"notify" => notify_deep(n()),
"sized" => sized(&a[2], a[3].parse().unwrap()),
"fieldborrow" => fieldborrow(a.get(2).is_some_and(|s| s == "string")),
_ => return false,
}
true
} |
|
Review of #739 at Net: 17 files, +5845/−3. Production: +3418 ( Read, not run. What I measured myself is the privacy search and the sums of the T14 logs. Round 3 BLOCKERs
Evidence at the head: PrivacyPasses. The local tables were unpacked into scratch and removed again. Every header OEM ID, OEM table ID and creator ID, every printable string of seven characters or more (3,670), every table length, and the machine-identifying tokens of the capture's boot log were searched for in the diff, the nine commit messages, the title and body and all nine comments: the hits are Microsoft's published Is the refusal rightFor the rule itself, yes. A bridge whose Secondary Bus Number is not above its own bus forwards no configuration access, so there is no bus number that reaches the device below it; the only access the interpreter could make instead is to another device's registers, and a store would write them. Refusing is the one answer that touches nothing it was not asked to. What it costs is not this pull request's to decide and is not yet measurable from it. A table that reads below such a bridge while it loads is refused whole, so on a machine whose firmware leaves that bridge at reset one SSDT's namespace is absent, where before it loaded on a value read from the wrong device. Firmware that does this is, by my memory of §6.5.4 ( BLOCKER
NOTE
SEND BACK |
The walk from a host bridge down to a PCI_Config region's device read offset 0x19 of every Device between them as a Secondary Bus Number. That register exists only in header layout 1. A function that is absent, as a root port the firmware disabled and hid is, answers all ones: 0xFF is above every bus, so the region's device was addressed on bus 255. A Device that is no bridge answers a byte of something else, and any value above its bus was taken. Each bridge is now asked for its Header Type first (offset 0x0E, the low seven bits), and a region below a function of any other layout is refused, Rule, with that one register read and nothing accessed below. ACPI 6.5 §6.5.4 was read for the rule and is cited at it: OSPM guarantees a PCI_Config region always accessible only "on a PCI root bus containing a _BBN object", and one below a bridge is ready "as soon the host controller or bridge controller has been programmed with a bus number". The bridge test's passing arm answers layout 1 at both bridges, the upper with the multi-function bit set; it gains four refusals: layout 0 with a plausible byte at 0x19, all ones at both, layout 2, and the multi-function bit alone. The track's bullet on the T14's tables gains an owner's exit a row can fail and what the out-of-tree check found with the bridges answered five ways. The QEMU issue's "nearest scope" is the nearest Device. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Round 5's mutations of the bridge rule, at The script: #!/bin/zsh
# Each mutation: checked, applied, built, its test run, restored. Run from the worktree root, on a clean tree.
S=<scratch>/aml-r5
M=userland/acpiserver/aml/Cargo.toml
T=aarch64-apple-darwin
out=$S/mutations/run.log
BRIDGE=regions:a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus
echo "head $(git rev-parse --short HEAD), $(git status --porcelain --ignore-submodules=none | wc -l | tr -d ' ') path(s) differ; $(uptime)" > $out
one() {
p=$1; shift
echo "=== $p" >> $out
git apply --check $S/mutations/$p.patch || { echo "CHECK FAILED" >> $out; return; }
git apply $S/mutations/$p.patch
cargo test --manifest-path $M --target $T --no-run > $S/mutations/$p.build.log 2>&1; echo "build EXIT=$?" >> $out
for spec in "$@"; do
bin=${spec%%:*}; name=${spec#*:}
cargo test --manifest-path $M --target $T --test $bin -- --exact $name > $S/mutations/$p.$name.log 2>&1; echo "$bin $name EXIT=$?" >> $out
grep -h "panicked at\|left:\|right:\|^0x" $S/mutations/$p.$name.log >> $out
done
git apply -R $S/mutations/$p.patch
echo "restored: $(git status --porcelain --ignore-submodules=none | wc -l | tr -d ' ') path(s) differ" >> $out
}
for p in $S/mutations/m*.patch; do one ${${p:t}%.patch} $BRIDGE; done
echo "final: $(git status --porcelain --ignore-submodules=none | wc -l | tr -d ' ') path(s) differ; $(uptime)" >> $out
echo done > $S/mutations/DONE
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..9d1f5f4f8 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -175,16 +175,10 @@ impl Machine<'_> {
/// the high word, function in the low), in the segment group the host
/// bridge's `_SEG` names or 0 without one (§6.5.6). The host bridge is
/// the nearest device naming a `_BBN`, which is the bus directly below it
- /// (§6.5.5); each device between it and the region's is a PCI-to-PCI
- /// bridge by its Header Type register, whose Secondary Bus Number
- /// register is the bus below it (PCI-to-PCI Bridge Architecture
- /// Specification 1.2, §3.2.5.4). A region declared in the host bridge
- /// itself addresses the bridge.
- ///
- /// §6.5.4 holds a PCI_Config region accessible always only on a root bus
- /// naming a `_BBN`, and one below a bridge once "the bridge controller
- /// has been programmed with a bus number": a region below anything else
- /// is refused, where any bus chosen for it would be another device's.
+ /// (§6.5.5); each device between it and the region's is a bridge, whose
+ /// Secondary Bus Number register is the bus below it (PCI-to-PCI Bridge
+ /// Architecture Specification 1.2, §3.2.5.4). A region declared in the
+ /// host bridge itself addresses the bridge.
///
/// Every access asks again, firmware's methods and the bridges both:
/// nothing is kept that a bridge renumbered since would make stale.
@@ -213,22 +207,11 @@ impl Machine<'_> {
let Some((&device, bridges)) = below.split_first() else { return self.function(host, segment, bus) };
for &bridge in bridges.iter().rev() {
let b = self.function(bridge, segment, bus)?;
- let register = |m: &mut Self, offset| {
- let at = Address::PciConfig { segment, bus, device: b.device, function: b.function, offset };
- m.host.read(at, crate::Access::Byte).map(|v| v as u8).map_err(|d| Error::Host(d.0))
- };
- // Offset 0x19 is a Secondary Bus Number only in header layout 1,
- // the low seven bits of the Header Type: a function that is
- // absent answers all ones, and any other layout a byte of
- // something else.
- if register(self, 0x0E)? & 0x7F != 0x01 {
- return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"));
- }
- let answered = register(self, 0x19)?;
- // §6.5.4: the region is ready once its bridge has a bus number.
- // The register resets to 0, and a bridge's secondary bus is
- // above the bus the bridge is on: any other answer would address
- // a device that is not below this bridge.
+ let secondary = Address::PciConfig { segment, bus, device: b.device, function: b.function, offset: 0x19 };
+ let answered = self.host.read(secondary, crate::Access::Byte).map_err(|d| Error::Host(d.0))? as u8;
+ // The register resets to 0, and a configured bridge's secondary
+ // bus is above the bus the bridge is on: any other answer would
+ // address a device that is not below this bridge.
if answered <= bus {
return Err(Error::Rule("a bridge's Secondary Bus Number is not above its own bus, and names no bus below it"));
}
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..98e97b387 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -211,7 +211,7 @@ impl Machine<'_> {
let mut bus = u8::try_from(bus).map_err(|_| Error::Rule("a _BBN above 0xFF (§6.5.5)"))?;
let segment = u16::try_from(segment).map_err(|_| Error::Rule("a _SEG above 0xFFFF (§6.5.6)"))?;
let Some((&device, bridges)) = below.split_first() else { return self.function(host, segment, bus) };
- for &bridge in bridges.iter().rev() {
+ for &bridge in bridges.iter() {
let b = self.function(bridge, segment, bus)?;
let register = |m: &mut Self, offset| {
let at = Address::PciConfig { segment, bus, device: b.device, function: b.function, offset };
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 9d1f5f4f8..8b0527b95 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -188,7 +188,7 @@ impl Machine<'_> {
let mut host = r.scope;
loop {
self.step()?;
- if matches!(self.ns.object(host), Some(Object::Device)) {
+ if true {
if self.ns.child(host, bbn).is_some() {
break;
}
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..11f6fac9b 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -221,7 +221,7 @@ impl Machine<'_> {
// the low seven bits of the Header Type: a function that is
// absent answers all ones, and any other layout a byte of
// something else.
- if register(self, 0x0E)? & 0x7F != 0x01 {
+ if false {
return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"));
}
let answered = register(self, 0x19)?;
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..a5d64b0d4 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -221,7 +221,7 @@ impl Machine<'_> {
// the low seven bits of the Header Type: a function that is
// absent answers all ones, and any other layout a byte of
// something else.
- if register(self, 0x0E)? & 0x7F != 0x01 {
+ if register(self, 0x0E).is_err() {
return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"));
}
let answered = register(self, 0x19)?;
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..34cc45828 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -221,7 +221,7 @@ impl Machine<'_> {
// the low seven bits of the Header Type: a function that is
// absent answers all ones, and any other layout a byte of
// something else.
- if register(self, 0x0E)? & 0x7F != 0x01 {
+ if register(self, 0x0E)? != 0x01 {
return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"));
}
let answered = register(self, 0x19)?;
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..4f7680606 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -221,7 +221,7 @@ impl Machine<'_> {
// the low seven bits of the Header Type: a function that is
// absent answers all ones, and any other layout a byte of
// something else.
- if register(self, 0x0E)? & 0x7F != 0x01 {
+ if register(self, 0x0E)? & 0x7F == 0x00 {
return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"));
}
let answered = register(self, 0x19)?;
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..7ef9dd35a 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -221,7 +221,7 @@ impl Machine<'_> {
// the low seven bits of the Header Type: a function that is
// absent answers all ones, and any other layout a byte of
// something else.
- if register(self, 0x0E)? & 0x7F != 0x01 {
+ if register(self, 0x0E)? == 0xFF {
return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"));
}
let answered = register(self, 0x19)?;
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..2a2fdbd96 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -221,7 +221,7 @@ impl Machine<'_> {
// the low seven bits of the Header Type: a function that is
// absent answers all ones, and any other layout a byte of
// something else.
- if register(self, 0x0E)? & 0x7F != 0x01 {
+ if !matches!(register(self, 0x0E)? & 0x7F, 0x01 | 0x02) {
return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"));
}
let answered = register(self, 0x19)?;
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..ef37e15a7 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -221,10 +221,10 @@ impl Machine<'_> {
// the low seven bits of the Header Type: a function that is
// absent answers all ones, and any other layout a byte of
// something else.
+ let answered = register(self, 0x19)?;
if register(self, 0x0E)? & 0x7F != 0x01 {
return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"));
}
- let answered = register(self, 0x19)?;
// §6.5.4: the region is ready once its bridge has a bus number.
// The register resets to 0, and a bridge's secondary bus is
// above the bus the bridge is on: any other answer would address
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..742f6ac07 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -229,7 +229,7 @@ impl Machine<'_> {
// The register resets to 0, and a bridge's secondary bus is
// above the bus the bridge is on: any other answer would address
// a device that is not below this bridge.
- if answered <= bus {
+ if false {
return Err(Error::Rule("a bridge's Secondary Bus Number is not above its own bus, and names no bus below it"));
}
bus = answered;
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..93f1413fe 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -229,7 +229,7 @@ impl Machine<'_> {
// The register resets to 0, and a bridge's secondary bus is
// above the bus the bridge is on: any other answer would address
// a device that is not below this bridge.
- if answered <= bus {
+ if answered < bus {
return Err(Error::Rule("a bridge's Secondary Bus Number is not above its own bus, and names no bus below it"));
}
bus = answered;
diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..52fc1a548 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -224,7 +224,7 @@ impl Machine<'_> {
if register(self, 0x0E)? & 0x7F != 0x01 {
return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"));
}
- let answered = register(self, 0x19)?;
+ let answered = register(self, 0x18)?;
// §6.5.4: the region is ready once its bridge has a bus number.
// The register resets to 0, and a bridge's secondary bus is
// above the bus the bridge is on: any other answer would addressIts log, |
|
The out-of-tree check behind the body's round 5 table. It is round 4's check (its source is the comment above that begins "The out-of-tree check behind the body's round 4 numbers") with the changes below, built with Commands, each
The host was shared: load averages near 28 while these ran. Changes to round 4's source, as 62,64c62,66
< /// Whether a bridge answers its Secondary Bus Number register as one
< /// configured would: the bus after its own.
< bridges: bool,
---
> bridges: Bridges,
> /// Reads of a function a listing does not hold.
> unlisted: u64,
> /// Header Type reads a listing answered: a bridge's layout, another, a function it does not hold.
> headers: [u64; 3],
66a69,113
> /// What the functions the interpreter asks as bridges answer at their Header
> /// Type (0x0E) and Secondary Bus Number (0x19) registers.
> #[derive(Clone, Default)]
> enum Bridges {
> /// Zero at both, as every other read.
> #[default]
> Zero,
> /// A bridge at reset: layout 1, secondary bus 0.
> Reset,
> /// A configured bridge: layout 1, the bus after its own.
> Configured,
> /// No function there: all ones at both.
> Absent,
> /// As a listing of a machine's functions reads: (bus, device, function)
> /// to the bytes at 0x0E, 0x18, 0x19 and 0x1A, every other register of a
> /// listed function zero. A function it does not list answers all ones to
> /// every read.
> Listed(BTreeMap<(u8, u8, u8), [u8; 4]>),
> }
>
> fn all_ones(w: Access) -> u64 {
> match w {
> Access::Byte => 0xFF,
> Access::Word => 0xFFFF,
> Access::DWord => 0xFFFF_FFFF,
> Access::QWord => u64::MAX,
> }
> }
>
> /// `SSSS:BB:DD.F class=... header_type=0xHH pri/sec/sub= PP SS UU`, a line a function.
> fn listing(path: &str) -> BTreeMap<(u8, u8, u8), [u8; 4]> {
> let hex = |s: &str| u8::from_str_radix(s.trim_start_matches("0x"), 16).unwrap();
> std::fs::read_to_string(path)
> .unwrap()
> .lines()
> .map(|l| {
> let f: Vec<&str> = l.split_whitespace().collect();
> let at: Vec<&str> = f[0].split([':', '.']).collect();
> assert!(at[0] == "0000" && f.len() == 7, "a line of another shape");
> let header = hex(f[2].strip_prefix("header_type=").unwrap());
> ((hex(at[1]), hex(at[2]), hex(at[3])), [header, hex(f[4]), hex(f[5]), hex(f[6])])
> })
> .collect()
> }
>
84c131,149
< Address::PciConfig { bus, offset: 0x19, .. } if self.bridges && w == Access::Byte => u64::from(bus) + 1,
---
> Address::PciConfig { bus, device, function, offset, .. } => match (&self.bridges, offset, w) {
> (Bridges::Reset | Bridges::Configured, 0x0E, Access::Byte) => 0x01,
> (Bridges::Configured, 0x19, Access::Byte) => u64::from(bus) + 1,
> (Bridges::Absent, 0x0E | 0x19, Access::Byte) => 0xFF,
> (Bridges::Listed(l), _, _) => match (l.get(&(bus, device, function)), offset, w) {
> (None, _, _) => {
> self.unlisted += 1;
> self.headers[2] += u64::from(offset == 0x0E && w == Access::Byte);
> all_ones(w)
> }
> (Some(r), 0x0E, Access::Byte) => {
> self.headers[usize::from(r[0] & 0x7F != 1)] += 1;
> u64::from(r[0])
> }
> (Some(r), 0x18..=0x1A, Access::Byte) => u64::from(r[usize::from(offset) - 0x17]),
> (Some(_), _, _) => 0,
> },
> _ => 0,
> },
116,117c181,182
< "reads mem/io/pci/ec {:?} writes {:?} sleeps {} stalls {} notifies {} locks {}",
< self.reads, self.writes, self.sleeps, self.stalls, self.notifies, self.locks
---
> "reads mem/io/pci/ec {:?} writes {:?} sleeps {} stalls {} notifies {} locks {} reads of an unlisted function {} listed header types bridge/other/absent {:?}",
> self.reads, self.writes, self.sleeps, self.stalls, self.notifies, self.locks, self.unlisted, self.headers
293c358
< fn tables(dir: &str, show: bool, mem: &BTreeMap<u64, u64>, bridges: bool) {
---
> fn tables(dir: &str, show: bool, mem: &BTreeMap<u64, u64>, bridges: &Bridges) {
314c379
< let mut h = Count { mem: mem.clone(), range, bridges, ..Count::default() };
---
> let mut h = Count { mem: mem.clone(), range, bridges: bridges.clone(), ..Count::default() };
352c417
< let mut h = Count { mem: mem.clone(), range, bridges, ..Count::default() };
---
> let mut h = Count { mem: mem.clone(), range, bridges: bridges.clone(), ..Count::default() };
385c450
< let mut h = Count { mem: mem.clone(), range, bridges, ..Count::default() };
---
> let mut h = Count { mem: mem.clone(), range, bridges: bridges.clone(), ..Count::default() };
579c644
< tables(&a[2], show, &mem, a.iter().any(|x| x == "--bridges"))
---
> tables(&a[2], show, &mem, &bridges(&a))
832a898,909
>
> fn bridges(a: &[String]) -> Bridges {
> let mut chosen = a.iter().filter_map(|x| match x.as_str() {
> "--bridges" => Some(Bridges::Configured),
> "--bridges=reset" => Some(Bridges::Reset),
> "--bridges=absent" => Some(Bridges::Absent),
> x => x.strip_prefix("--pci=").map(|p| Bridges::Listed(listing(p))),
> });
> let one = chosen.next().unwrap_or_default();
> assert!(chosen.next().is_none(), "one answer for the bridges");
> one
> } |
|
Review of #739 at Net: 17 files, +5893/−3. Production: +3435 ( Read, not run. What I measured myself: the privacy search, the diff of the six out-of-tree runs between the two heads, and the cited sentences against the saved chapter of the specification. Round 4 BLOCKERCLOSED. Offset 0x19 read from every Device above the region's without asking whether it is a bridge.
Round 4 NOTEs
Evidence at the head
The listing run, and whether the refusal is rightThe measurement holds as stated. With the bridges answered from a local listing of the real machine's functions under Linux, all 14 tables load at both heads. At Refusing is right. A function that is not there forwards nothing, so there is no bus below it: any bus the interpreter chose would be another device's or nobody's, and the previous head chose 255 from the all-ones answer. The one other answer open to it, all ones for a read and a dropped store without asking the host, is the interpreter inventing a device's answer, a silent default. §6.5.4 puts the firmware outside what it may assume: only PCI_Config on a root bus naming a What the sweep cannot show is whether any of the 42 is a method the server must run. The sweep calls every method without an argument; a start-up walk that stops at a parent that is not present never reaches a child's method, and a wake or event handler that probes below a port from outside it would. That is the later stage's to find, and the track does not yet make it read: under NOTE. PrivacyThe tree, the eleven commit messages, the title and the two comments posted this round pass. Searched for: every address, class and bus triple of the listing, every table length and their sums, and any hex literal of five digits or more in this round's material. The hits are small decimals coinciding with benchmark figures. The test's bus numbers 0x40, 0x45, 0x47 and 0x3F are none of the listing's, and the harness comment shows the listing's line format with placeholders only. The body fails on one figure: under NOTE. BLOCKERNone. NOTE
LAND AFTER NAMED CHANGES |
The exit read table loads alone, so a method the server evaluates on the machine and has refused for a bridge's answer reached nobody by it. The same T14 row now reads that none was, and one that was goes to the owner with the table refusal. Review round 5's NOTE on #739. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
userland/Cargo.toml conflicted: #713 added the member `acpiserver` on the line this branch added `acpiserver/aml`. Both hold, in order. The track merged without a conflict and came out with two stages named "the interpreter": #713's, which the ACPI server runs and which owns the T14's press issue, and this branch's, the crate and its host exit. The press issue names its owner as the stage "the interpreter", so they are folded into one stage carrying both exits. This branch's "inside the server that uses it" is dropped with the fold: /system/bin/acpiserver now exists and does not link the crate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
#739 added userland/acpiserver/aml to userland's member list and its three path packages to userland's lock, both of which this branch deletes. The member joins the root list as userland/acpiserver/aml; the root lock gains toyos-aml, and already held toyos-acpi and toyos-bootmap. Its name and version pairs are again the union of main's five locks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
What this changes
Adds
userland/acpiserver/aml, packagetoyos-aml: the ACPI Machine Language interpreter, as a pure library. It loads a machine's DSDT and SSDTs into one namespace and evaluates its objects. It has no hardware access of its own, is#[no_std]and#[forbid(unsafe_code)], and has one dependency,toyos-acpi, for the table it takes. It also adds the crate's line inuserland/Cargo.tomland its lock entry, the crate and its host exit in the track's interpreter stage, and one issue file.toyos_acpi::Table.Table::openalready checks §5.2.6's header, length and checksum for every table, so the interpreter has no check of its own.Hosttrait, in four spaces:_ADR,_BBNand_SEG(§6.1.1, §6.5.5, §6.5.6), each refused rather than truncated when out of range. The device is the nearest one the region's scope is or lies in, so a region a method declares addresses the device the method is in. A device below bridges is on the bus the nearest bridge's Secondary Bus Number register names (PCI-to-PCI Bridge Architecture Specification 1.2, §3.2.5.4), each bridge read through theHoston the bus the one above it named. Each is asked first for its Header Type register: a function whose layout is not a PCI-to-PCI bridge's, or that is absent and answers all ones, has no such register and the region below it is refused. A register that answers a bus not above the bridge's own, as an unconfigured bridge's reset value of 0 does, names no bus and is refused. ACPI §6.5.4 is the reason for both and is cited at the rule. Nothing is kept from one access to the next;Meterbounds at 16 MiB the sum of what a table sizes: every string's, buffer's and loaded table's bytes, every package element, every namespace node, and every package element's name not yet defined, with its segments. That is the meter's count. Each node and element carries a constant beside it that the meter does not count; the heap behind a full meter is measured below and recorded in the track.Ref::Slotis aWeak: the frame alone holds its slots, so a reference whose method has exited names nothing (NotFound), as a reference to a named object that is gone does. A reference to a package element holds its package and is refused entry to a package or a named object. No reference owns another, so no chain of them forms and none is part of a cycle.\_OSIsays yes to the 22 Windows version strings Microsoft publishes and no to everything else, the ACPI feature groups included. The source is Microsoft's page "How to Identify the Windows Version in ACPI by Using _OSI", which says "Windows supports _OSI only for the use of identifying the host version of Windows".\_OSis "Microsoft Windows NT".\_REVis 2.Not yet:
Load,LoadTableandDataTableRegion. The T14's tables hold eightLoadopcodes and oneLoadTable; none runs while a table loads._BBN, which is QEMU's:issues/qemus-interrupt-links-are-refused-by-the-aml-interpreter.md._REG/_INI/_STAstart-up sequence.acpiserver.Why
This is the interpreter stage of
issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md. #713 placed that stage, run by the ACPI server and owning the T14's press issue; this PR names the crate in it and puts the host half of its exit before the T14 half, which stays open. It records the owner's three rulings of 2026-10-05 verbatim, and records his words on the clean room as the orchestrator's record of 2026-10-04 holds them. Under the stage it records what the T14 check found that a later stage would otherwise pay to find again, and the two weaknesses of the meter with their owner and exit.issues/the-tree-says-who-uses-each-thing.md: inside the ACPI server, its first user, ascompositor/desktopandsoundserver/mixersit inside theirs.src/userlandhost.rs's survey gates the tests, andsrc/clippy.rs's nested run lints them.The merge of
origin/mainat257ebea2a(#713, ACPI stage 1)d5c0062dcmerges it;git show --remerge-diff d5c0062dcnames two files and no other.userland/Cargo.tomlconflicted. ACPI stage 1: the machine in ACPI mode for a userland server's claim; its SCI, power button and EC events served #713 added the memberacpiserveron the line this branch addedacpiserver/aml. Both hold, in that order.userland/Cargo.lockmerged by itself: the build and the host gate below ran on it and left the tree clean.issues/the-t14s-power-button-event-came-up-to-17-s-after-ec-query-0x28.md, its exit on the T14; and this branch's, the crate, its exit the host test and the out-of-tree check. The press issue names its owner as the stage "the interpreter", which two stages cannot answer. They are folded into one: ACPI stage 1: the machine in ACPI mode for a userland server's claim; its SCI, power button and EC events served #713's placement and ownership word for word, the crate named in it, and one exit that is this branch's then ACPI stage 1: the machine in ACPI mode for a userland server's claim; its SCI, power button and EC events served #713's, each clause as its side wrote it. Nothing else of either side moved: ACPI stage 1: the machine in ACPI mode for a userland server's claim; its SCI, power button and EC events served #713's rulings on tests, stage 1's exit and what it leaves unread, the stopgap ruling and stage 1's design; this branch's two rulings, the four findings with their owners and exits, and the power-off stage.mainand is gone with the fold: "inside the server that uses it"./system/bin/acpiservernow exists and does not link the crate; the stage says "beside the server, which does not link it yet". This branch says nothing else about stage 1.userland/acpiserver/src/aml.rsis the server's own module, a stub that serves no query; the crate is the directoryuserland/acpiserver/amlbeside it. Neither names the other.Round 5: what review round 4 came to
BLOCKER, offset 0x19 read as a Secondary Bus Number without asking whether the function is a bridge: holds, and fixed. Measured at
98ffa8753on the T14's tables with every function the walk asks answering all ones, as an absent one does: all 14 tables loaded and the sweep refused no method, every region below such a function addressed on bus 255. At this head the same run refuses one SSDT and 50 methods,Rule.NOTEs.
eea9ec35c: the same row reads that no method the server evaluated was refused for a bridge's answer, and one that was goes to the owner with the table refusal.98ffa8753's commit message, which is pushed and left as it is._BBN" is "nearest Device".Round 4: what each finding of review round 3 came to
Each was measured at
488a05a57before it was changed: by the out-of-tree check, whose source and commands are in a comment below, and by the new tests run against the old source (EXIT=101, four tests red). The host was shared and loaded throughout; times are single runs of a release build.BLOCKER 1, a field store borrowing its source while firmware runs: holds, and fixed. The review's construction, a
_ADRmethod that stores to the buffer being stored, panicked at488a05a57withRefCell already borrowed, exit 101, for a Buffer and for a String source. Now both return, exit 0. The store writes from a copy of its own, so what is written is what the source held when the store began.a_field_store_writes_its_source_as_it_was_when_firmware_changes_itasserts the return, the bytes written and the source afterwards, for both. The String-to-field store of round 2 now peaks at 233,191 bytes of heap, from 167,680: the copy.BLOCKER 2, an unresolved element's
Pathoutside the meter: holds, and fixed.fill-lazywith 255-segment names at488a05a57: 713,799,584 bytes of heap behind a full meter, across 631 tables, none of them kept. The review's arithmetic said about 770 MB. The element is now anUnresolved, metered at its own size and its segments'. The heap an interpreter held with its meter full, by what filled it:488a05a57tables_and_names_are_held_against_the_live_boundgains the case: seventeen tables of 1,020 such elements, refused by the fifteenth. What remains is the constant beside each node and element, 2.5 times the meter for field units, and the arena a refused load leaves at capacity, 24,115,888 bytes after one table of 204,000 field units was refused. Both are in the track beside "A refused evaluation keeps what it stored", with these numbers, the power-off stage as owner, and an exit.BLOCKER 3, the secondary bus used unchecked and kept: decided both ways it was open.
Rule. At488a05a57the new test's unconfigured bridge was read as bus 0 and the endpoint's field answered from there._ADR,_BBNand_SEGagain, so a bridge renumbered since is seen and no issue is needed for one that is not. The cost on the T14's tables: the sweep of 1,543 methods made 358 PCI_Config reads where it made 331.a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bushas two bridges, the upper read on the_BBNbus and the lower on the bus the upper answered. Deleting.rev()turns it red, exit 101; so do taking any answer, taking an answer equal to the bridge's own bus, and asking for offset 0x18.BLOCKER 4, name resolution charged no step: holds, and wider than named. A loop of one operation, run until the step bound refused it:
488a05a57Scopeover an alias across 25 tablesNotifyof a device 240 scopes down, which writes its path outEvery walk of the namespace now pays its caller a step for each scope climbed and each segment looked up: resolution, a definition's path, and a path written out for
Notify, a refusal or a reference handed to the caller. A NameString is charged for its bytes where it is read. A path the caller ofevaluatewrites is the caller's and pays nothing.Reading every operator for the same defect found it on six more paths, each an operator that reads all of a long object to make something short, and one copy that was not needed. Measured on objects of 64 KiB, a sixteenth of the largest:
488a05a57_OSIof a long string, which was copied to be comparedEach of the six is now charged for what it reads.
_OSIno longer copies, so it does no work its argument sizes and is charged nothing more. These six were not in the review; they are the same finding, and leaving them would have left "a step for every 64 bytes of work" false.a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_itholds twelve such loops, each counted by a named integer the loop increments, to no more iterations than the step bound over the charge. It does not rest onBoundbeing returned, which every loop reaches with or without its charge. Against the old source all twelve ran past their count; each charge removed alone turns its own case red.The slowest evaluation among those measured at this head is the ToInteger loop, 78 ms. That is the slowest found, not a proof of the worst.
NOTEs.
RP03,PXSXand their_ADRare gone with the test that held them; the bridge test namesBRG0,BRG1andEND0. Every NameSeg in the tests was then checked against the local tables and againstmain:LPCB,LPCRand three names the tests had invented that happened to occur there were renamed too. Review round 4 found five more NameSegs of the tests that occur in those tables as names and not onmain, all generic words that identify nothing; the claim this note made of what is still shared was false and is withdrawn.Earlier rounds' findings and what closed them are in the comments, at the heads they were measured on.
The T14's tables
Run by a check outside the tree, on the owner's local copy: the DSDT and the 13 SSDTs beside it. The interpreter ran against a host that answers reads from nothing: this machine's memory was not available, only its tables. Every read is answered zero except as each row says. The first row's result is the host's answer, not the table's: the DSDT's definition-block code reads a chipset-series word from SystemMemory and branches on it in two places, and at zero the two branches contradict. Every other row answers that one 16-bit word 2.
Six ways, each run at
98ffa8753and at this head, every one exit 0:98ffa8753NotFound; nothing loads"Refused" counts methods in the sweep below, each for a bridge's answer. In every row that loads the DSDT,
\_S0,\_S3,\_S4and\_S5each evaluate to a package of four, and the first element of\_S5equalstoyos_acpi::s5_slp_typof the same DSDT.The sweep. A byte scan found 2,175 method definitions, 1,543 without an argument, each evaluated once. Against configured bridges at this head: 1,068 gave a value; for 471 the scanned path named no object; 2 were refused for a name no table defines; 1 executed
Fatal; 1 was refused for the SystemCMOS space. Those are round 3's counts exactly. The sweep made 409 PCI_Config reads where it made 358, the Header Type reads. While loading, the tables made 294 SystemMemory reads and 52 PCI_Config reads, no write, and 404 Global Lock calls.The listing. The orchestrator's local reading of every PCI function of the T14 under Linux: its Header Type and its primary, secondary and subordinate bus bytes. Six of them are bridges, each with a secondary bus above its own. The host answered those four registers from it and zero for every other register of a listed function; a function it does not list answered all ones to every read. At this head the sweep asked 51 Header Types: 9 answered a bridge's layout, none another layout, and 42 were of a function the listing does not hold. Those 42 are the 42 refusals: each refused walk met a function the listing does not hold. At
98ffa8753each of those was read as a secondary bus of 255 and its region addressed there.What the refusal costs on the real machine is not shown by any of this. The listing is a reading under Linux, taken after Linux enumerated the buses, and Linux may have numbered a bridge the firmware left unnumbered: it does not show what the firmware leaves at boot. If the firmware leaves the one bridge that SSDT reads below present and numbered, all 14 tables load; if it leaves it unnumbered or hidden, that SSDT is refused. Which it is takes one read on the machine at boot. The track's bullet now owes it, with an exit.
What else this does not show: the tables against the machine's own memory. Every answer but those named was zero, so every branch on firmware state took its zero side.
Evidence
Head
d5c0062dc, the merge oforigin/mainat257ebea2aintoeea9ec35c. The three gates below ran at this head. Everything else in this section and in "The T14's tables" was measured at7d130196a, and "this head" there means it:git diff --stat 7d130196a d5c0062dc -- userland/acpiserver/amlis empty, so no source or test of the crate differs. Its one dependency does: #713 changedtoyos-acpi, and the crate's tests are run again on it below.cargo run -- --ci host:EXIT=0. The lines below are the gate's own, each whole, chosen from its log of 7,736 lines, which is in the orchestrator's scratch asaml-r7/ci-host-d5c0062dc.log.The crate's host tests,
cargo test --manifest-path userland/acpiserver/aml/Cargo.toml --target aarch64-apple-darwin:EXIT=0, logaml-r7/crate-d5c0062dc.log. 27 inevaluate, 22 inhostile, 18 innamespace, 17 inregions.cargo run -- --build-only:EXIT=0, logaml-r7/build-only-d5c0062dc.log, 742 lines. Run becauseuserland/Cargo.tomland its lock now hold both #713's server and this crate: the userland workspace resolves,acpiservercompiles andbin/acpiserveris in the image.git status --porcelain --ignore-submodules=noneprinted nothing after the three gates, so no build rewrote the lock.Negative control and mutations of the bridge rule, at this head, each a checked patch, built, run and restored by one script, the tree clean after each; patches and the script's log in a comment below. Every build
EXIT=0, anda_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_busEXIT=101under each:Round 4's other mutations, at
98ffa8753, in the comments above and not run again: the source they mutate is unchanged since. A field store borrowing its source turns the field-store test red; an unresolved name held at no size, and its segments not counted, the meter test; each of the twelve charges removed alone, its own case of the walk test and no other.Oracles. QEMU's own DSDT, whose boot logged
ACPI: PM1a=0x604 SLP_TYPa=0;toyos_acpi::s5_slp_typ, a second reader of the same bytes, on QEMU's DSDT in the tree and on the T14's outside it; QEMU's CPU hotplug register block and the HPET specification's capability register, for the two methods run; the T14's tables; for the bridge rule, ACPI §6.5.4 read from the text, and a reading of the T14's own PCI functions under Linux. The Header Type layout and the Secondary Bus Number's offset are from the PCI-to-PCI Bridge Architecture Specification as I know it: I had no copy to read, and the section number the source cites for the register is unchecked.Earlier rounds, at the heads they were measured on. Round 1's ten controls at
be10f3bc, round 2's four mutations at0af9f964, round 3's four at488a05a57and round 4's twenty-one at98ffa8753are in the comments above.The guest suite: not run. No program depends on the crate, so no guest holds it.
Size. Against
origin/mainat257ebea2a: 17 files, +5898/−8 — production 3435 (src/), tests 2309, issues, manifests and lock +154/−8. Round 5: 4 files, +79/−31; the named change after it: 1 file, +3.What is unsure
_OSIand feature groups rest on one sentence of Microsoft's page. The T14's tables ask about one feature group, and what Windows answers to it was not measured on Windows.XYZexample contradicts §19.6.20's definition. The code follows the definition, and the test says so where it asserts.\_REVis 2, what Windows answers.Anything a reader of
mainmust not missacpiserver. Stage 1's server,userland/acpiserver, answers every embedded-controller query with nothing (userland/acpiserver/src/aml.rs), and the power-off stage is the first consumer of\_S5.🤖 Generated with Claude Code