Repository navigation
The AML interpreter finished to ACPI 6.5: Load and _OSC, the namespace initialised, every address space, every event, and the bounds stated - #875
Conversation
The owner's "finish aml then continue working on deep idle states" (#870, comment 6119715385) puts the interpreter ahead of deep idle states. The track now says what finished is, held to ACPI 6.5 and to Intel's 302223-007 for a processor's _OSC, and the six slices that reach it in order, each with its exit: Load and _OSC first, which deep idle waits on. The design, its citations and its roast are #875's body. The track is assigned, S1 held by #875, and its Load bullet now names its owner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
|
Owner's ruling on what AML may reach (stage S2), recorded by the orchestrator. Asked whether the interpreter may read the device memory the firmware's own tables name (the AMD bench's DSDT needs it), checked against the firmware's declared reserved ranges, he answered: "if thats the best way to do it then yes". So the permission is conditional on S2's design showing that this check is the best one: it must be held to the specification, it must never reach RAM or a claimed device's BARs, and everything else must be refused by name. If the design finds a better boundary, it brings that to him instead. M1's dumps are taken (default boot on both benches, steps 1–8 of the request). They are on the development host under |
|
Design acceptance, recorded by the orchestrator for the owner. He was shown the design's decisions:
He answered: "do the full aml in one pr". So the design is accepted, with one change: all six stages, S1–S6, are built and land as this one pull request, not as six. The M1 check's K1–K5 still decide the order inside it. On the ECDT conflict (ACPI 6.5 §6.5.4 against his earlier "stopgap, delete later"), he ruled follow the spec. ECDT support is permanent, spec-required behaviour. His ruling on what AML may reach (S2) is in the comment above. |
|
M1, part 1 of 3: the T14's log, whole. |
|
M1, part 2 of 3: the AMD bench's log, whole. |
|
M1, part 3 of 3: the harness, whole. A scratch crate outside the tree that never lands. It depends by path on this PR's worktree at
[package]
name = "m1-harness"
version = "0.0.0"
edition = "2024"
publish = false
[dependencies]
toyos-aml = { path = "<the worktree>/system/acpiserver/aml" }
toyos-acpi = { path = "<the worktree>/lib/acpi" }
[workspace]
//! M1 (PR #875 section 5): ToyOS's interpreter as it stands, outside the tree,
//! with a tracing host, on one bench's dump. Writes two files: `local.txt`,
//! which carries addresses and stays on this host, and `summary.txt`, counts
//! and decoded values only.
//!
//! The host answers a SystemMemory read inside a dumped NVS range from the
//! dump (an overlay that the run's own writes update); every other read is
//! unrecorded and is refused (`deny`) or answered 0 (`zero`). Every write is
//! accepted and recorded.
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;
use std::fs;
use std::path::{Path, PathBuf};
use toyos_aml::{Access, Address, Denied, Error, Host, Interpreter, Kind, Value};
#[derive(Clone)]
struct Range {
start: u64,
end: u64,
label: String,
}
fn memmap(dir: &Path) -> Vec<Range> {
let text = fs::read_to_string(dir.join("memmap.txt")).expect("memmap.txt");
text.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| {
let mut it = l.splitn(3, ' ');
let hex = |s: &str| u64::from_str_radix(s.trim_start_matches("0x"), 16).expect("hex");
let start = hex(it.next().unwrap());
let end = hex(it.next().unwrap());
Range { start, end, label: it.next().unwrap().trim().to_string() }
})
.collect()
}
fn short(label: &str) -> &'static str {
match label {
"System RAM" => "RAM",
"Reserved" => "Reserved",
"ACPI Non-volatile Storage" => "NVS",
"ACPI Tables" => "ACPI-reclaim",
"Unusable memory" => "Unusable",
"Persistent Memory" | "Persistent Memory (legacy)" => "Persistent",
"Soft Reserved" => "SoftReserved",
_ => "other-label",
}
}
fn mem_class(map: &[Range], at: u64, len: u64) -> &'static str {
let last = at.saturating_add(len.saturating_sub(1));
match map.iter().find(|r| r.start <= at && at <= r.end) {
Some(r) if last <= r.end => short(&r.label),
Some(_) => "straddles-ranges",
None => "unlisted",
}
}
#[derive(Clone)]
struct Nvs {
base: u64,
bytes: Vec<u8>,
}
fn nvs_dumps(dir: &Path) -> Vec<Nvs> {
let mut out = Vec::new();
for e in fs::read_dir(dir.join("nvs")).expect("nvs/") {
let p = e.unwrap().path();
if p.extension().and_then(|x| x.to_str()) != Some("bin") {
continue;
}
let bytes = fs::read(&p).unwrap();
if bytes.is_empty() {
continue;
}
let stem = p.file_stem().unwrap().to_str().unwrap();
let base = u64::from_str_radix(stem.trim_start_matches("0x"), 16).unwrap();
out.push(Nvs { base, bytes });
}
out
}
/// The FADT's ports, by the role the FADT gives them (ACPI 6.5 §5.2.9, Table 5.9).
fn fadt_ports(dir: &Path) -> Vec<(u64, u64, &'static str)> {
let f = fs::read(dir.join("tables/FACP")).expect("FACP");
let u32_at = |o: usize| u32::from_le_bytes(f[o..o + 4].try_into().unwrap()) as u64;
let u8_at = |o: usize| f[o] as u64;
let mut out = vec![(u32_at(48), 1, "SMI_CMD")];
let blocks: [(usize, usize, Option<usize>, &str); 8] = [
(56, 88, Some(148), "PM1a_EVT"),
(60, 88, Some(160), "PM1b_EVT"),
(64, 89, Some(172), "PM1a_CNT"),
(68, 89, Some(184), "PM1b_CNT"),
(72, 90, Some(196), "PM2_CNT"),
(76, 91, Some(208), "PM_TMR"),
(80, 92, Some(220), "GPE0"),
(84, 93, Some(232), "GPE1"),
];
for (legacy, len, x, role) in blocks {
let mut at = u32_at(legacy);
if at == 0 {
if let Some(x) = x {
if f.len() >= x + 12 && f[x] == 1 {
at = u64::from_le_bytes(f[x + 4..x + 12].try_into().unwrap());
}
}
}
let n = u8_at(len);
if at != 0 && n != 0 {
out.push((at, n, role));
}
}
if f.len() >= 129 && f[116] == 1 {
out.push((u64::from_le_bytes(f[120..128].try_into().unwrap()), 1, "RESET_REG"));
}
out
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Mode {
Deny,
Zero,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Rec {
space: &'static str,
write: bool,
width: u8,
at: u64,
class: String,
value: u64,
answered: &'static str,
}
struct Tracer {
map: Vec<Range>,
ports: Vec<(u64, u64, &'static str)>,
overlay: Vec<Nvs>,
mode: Mode,
recs: Vec<Rec>,
notifies: Vec<(String, u64)>,
slept_ms: u64,
stalled_us: u64,
t: u64,
}
fn bytes_of(w: Access) -> u8 {
match w {
Access::Byte => 1,
Access::Word => 2,
Access::DWord => 4,
Access::QWord => 8,
}
}
impl Tracer {
fn new(map: &[Range], ports: &[(u64, u64, &'static str)], nvs: &[Nvs], mode: Mode) -> Self {
Tracer { map: map.to_vec(), ports: ports.to_vec(), overlay: nvs.to_vec(), mode, recs: Vec::new(), notifies: Vec::new(), slept_ms: 0, stalled_us: 0, t: 0 }
}
fn port_class(&self, p: u16, n: u8) -> String {
let p = p as u64;
for &(at, len, role) in &self.ports {
if p >= at && p + n as u64 <= at + len {
return role.to_string();
}
}
"a port the FADT does not name".to_string()
}
fn classify(&self, at: Address, n: u8) -> (&'static str, u64, String) {
match at {
Address::Memory(a) => ("SystemMemory", a, mem_class(&self.map, a, n as u64).to_string()),
Address::Io(p) => ("SystemIO", p as u64, self.port_class(p, n)),
Address::PciConfig { segment, bus, device, function, offset } => (
"PCI_Config",
(segment as u64) << 40 | (bus as u64) << 32 | (device as u64) << 24 | (function as u64) << 16 | offset as u64,
"configuration space".to_string(),
),
Address::EmbeddedControl(o) => ("EmbeddedControl", o as u64, "EC space".to_string()),
}
}
fn in_nvs(&mut self, a: u64, n: u8) -> Option<(usize, usize)> {
self.overlay.iter().enumerate().find_map(|(i, d)| {
let off = a.checked_sub(d.base)?;
(off + n as u64 <= d.bytes.len() as u64).then_some((i, off as usize))
})
}
}
impl Host for Tracer {
fn read(&mut self, at: Address, width: Access) -> Result<u64, Denied> {
let n = bytes_of(width);
let (space, addr, class) = self.classify(at, n);
if let Address::Memory(a) = at {
if let Some((i, off)) = self.in_nvs(a, n) {
let mut v = 0u64;
for k in 0..n as usize {
v |= (self.overlay[i].bytes[off + k] as u64) << (8 * k);
}
self.recs.push(Rec { space, write: false, width: n, at: addr, class, value: v, answered: "dump" });
return Ok(v);
}
}
match self.mode {
Mode::Deny => {
self.recs.push(Rec { space, write: false, width: n, at: addr, class, value: 0, answered: "denied" });
Err(Denied("unrecorded".into()))
}
Mode::Zero => {
self.recs.push(Rec { space, write: false, width: n, at: addr, class, value: 0, answered: "zero" });
Ok(0)
}
}
}
fn write(&mut self, at: Address, width: Access, value: u64) -> Result<(), Denied> {
let n = bytes_of(width);
let (space, addr, class) = self.classify(at, n);
if let Address::Memory(a) = at {
if let Some((i, off)) = self.in_nvs(a, n) {
for k in 0..n as usize {
self.overlay[i].bytes[off + k] = (value >> (8 * k)) as u8;
}
}
}
self.recs.push(Rec { space, write: true, width: n, at: addr, class, value, answered: "accepted" });
Ok(())
}
fn sleep(&mut self, ms: u64) {
self.slept_ms += ms;
self.t += ms * 10_000;
}
fn stall(&mut self, us: u64) {
self.stalled_us += us;
self.t += us * 10;
}
fn timer(&mut self) -> u64 {
self.t += 10;
self.t
}
fn notify(&mut self, object: &str, value: u64) {
self.notifies.push((object.to_string(), value));
}
fn global_take(&mut self) -> Result<(), Denied> {
Ok(())
}
fn global_take_within(&mut self, _ms: u16) -> Result<bool, Denied> {
Ok(true)
}
fn global_release(&mut self) -> Result<(), Denied> {
Ok(())
}
}
/// ToUUID's byte order (ACPI 6.5 §19.6.142).
fn uuid(s: &str) -> Vec<u8> {
let h: Vec<u8> = s.split('-').flat_map(|g| (0..g.len()).step_by(2).map(move |i| u8::from_str_radix(&g[i..i + 2], 16).unwrap())).collect();
let mut out = vec![h[3], h[2], h[1], h[0], h[5], h[4], h[7], h[6]];
out.extend_from_slice(&h[8..16]);
out
}
const PLATFORM: &str = "0811B06E-4A27-44F9-8D60-3CBBC22E7B48";
const INTEL: &str = "4077A616-290C-47BE-9EBD-D87058713953";
struct Bench {
dir: PathBuf,
map: Vec<Range>,
ports: Vec<(u64, u64, &'static str)>,
nvs: Vec<Nvs>,
tables: Vec<(String, Vec<u8>)>,
}
fn tables(dir: &Path) -> Vec<(String, Vec<u8>)> {
let mut ssdts: Vec<(u32, String)> = fs::read_dir(dir.join("tables"))
.unwrap()
.filter_map(|e| {
let n = e.unwrap().file_name().into_string().unwrap();
n.strip_prefix("SSDT").map(|k| (k.parse::<u32>().unwrap_or(0), n.clone()))
})
.collect();
ssdts.sort();
let mut out = vec![("DSDT".to_string(), fs::read(dir.join("tables/DSDT")).unwrap())];
for (_, n) in ssdts {
out.push((n.clone(), fs::read(dir.join("tables").join(&n)).unwrap()));
}
out
}
fn kind(e: &Error) -> String {
match e {
Error::Malformed { why, .. } => format!("Malformed({why})"),
Error::NotFound(_) => "NotFound".into(),
Error::Exists(_) => "Exists".into(),
Error::Type(w) => format!("Type({w})"),
Error::Rule(w) => format!("Rule({w})"),
Error::Table(w) => format!("Table({w})"),
Error::Fatal { .. } => "Fatal".into(),
Error::Bound(w) => format!("Bound({w})"),
Error::Unsupported(w) => format!("Unsupported({w})"),
Error::Host(w) => format!("Host({w})"),
Error::Bridge { .. } => "Bridge".into(),
}
}
impl Bench {
fn open(dir: &Path) -> Bench {
Bench { dir: dir.to_path_buf(), map: memmap(dir), ports: fadt_ports(dir), nvs: nvs_dumps(dir), tables: tables(dir) }
}
fn host(&self, mode: Mode) -> Tracer {
Tracer::new(&self.map, &self.ports, &self.nvs, mode)
}
/// Every table loaded into a fresh interpreter, in order; each one's result.
/// The load answers an unrecorded read 0, whatever the run's mode: on
/// metal the kernel answers it, and the dump does not hold it.
fn load(&self, host: &mut Tracer) -> (Interpreter, Vec<(String, Result<(), Error>)>) {
let mode = host.mode;
host.mode = Mode::Zero;
let r = self.load_in(host);
host.mode = mode;
r
}
fn load_in(&self, host: &mut Tracer) -> (Interpreter, Vec<(String, Result<(), Error>)>) {
let mut i = Interpreter::new();
let mut results = Vec::new();
for (name, bytes) in &self.tables {
let sig: [u8; 4] = bytes[..4].try_into().unwrap();
let r = match toyos_acpi::Table::open(bytes.as_slice(), 0, &sig, 36) {
Ok(t) => i.load(host, &t),
Err(e) => Err(Error::Table(Box::leak(format!("{e:?}").into_boxed_str()))),
};
results.push((name.clone(), r));
}
(i, results)
}
}
fn walk(i: &Interpreter) -> Vec<(String, u32, Kind)> {
let mut w = i.walk().expect("walk");
let mut out = Vec::new();
while let Some(e) = w.next() {
out.push((w.path().to_string(), e.depth, e.kind));
}
out
}
fn args(uuid_text: &str, caps: &[u32]) -> Vec<Value> {
let mut b = Vec::new();
for c in caps {
b.extend_from_slice(&c.to_le_bytes());
}
vec![Value::Buffer(uuid(uuid_text)), Value::Integer(1), Value::Integer(caps.len() as u64), Value::Buffer(b)]
}
fn decode(v: &Result<Value, Error>) -> String {
match v {
Ok(Value::Buffer(b)) if b.len() == 8 => {
let d1 = u32::from_le_bytes(b[0..4].try_into().unwrap());
let d2 = u32::from_le_bytes(b[4..8].try_into().unwrap());
format!("Buffer(8): DWORD1={d1:#x} DWORD2={d2:#x}")
}
Ok(Value::Buffer(b)) => format!("Buffer({})", b.len()),
Ok(other) => format!("{other:?}"),
Err(e) => format!("refused: {}", kind(e)),
}
}
struct RunOut {
inis: Vec<(String, String)>,
result: Result<Value, Error>,
recs: Vec<Rec>,
notifies: Vec<(String, u64)>,
slept_ms: u64,
stalled_us: u64,
}
fn run(b: &Bench, mode: Mode, inis: &[String], target: &str, a: &[Value]) -> RunOut {
let mut h = b.host(mode);
let (mut i, _) = b.load(&mut h);
let mut ini_out = Vec::new();
for p in inis {
let r = i.evaluate(&mut h, p, &[]);
ini_out.push((p.clone(), match r { Ok(_) => "ran".into(), Err(e) => kind(&e) }));
}
let mark = h.recs.len();
let n0 = h.notifies.len();
let (s0, st0) = (h.slept_ms, h.stalled_us);
let result = i.evaluate(&mut h, target, a);
RunOut {
inis: ini_out,
result,
recs: h.recs[mark..].to_vec(),
notifies: h.notifies[n0..].to_vec(),
slept_ms: h.slept_ms - s0,
stalled_us: h.stalled_us - st0,
}
}
fn tally(recs: &[Rec]) -> BTreeMap<String, usize> {
let mut m = BTreeMap::new();
for r in recs {
let k = format!("{} {} {}B {} [{}]", r.space, if r.write { "write" } else { "read" }, r.width, r.class, r.answered);
*m.entry(k).or_insert(0) += 1;
}
m
}
/// K1's classes: a write to SMI_CMD, to configuration space, or to memory
/// outside NVS and reserved.
fn k1_writes(recs: &[Rec]) -> Vec<&Rec> {
recs.iter()
.filter(|r| r.write)
.filter(|r| match r.space {
"SystemIO" => r.class == "SMI_CMD",
"PCI_Config" => true,
"SystemMemory" => !(r.class == "NVS" || r.class == "Reserved"),
_ => false,
})
.collect()
}
fn scan_ops(bytes: &[u8]) -> BTreeMap<&'static str, Vec<usize>> {
let mut m: BTreeMap<&'static str, Vec<usize>> = BTreeMap::new();
for k in ["Load 5B20", "LoadTable 5B1F", "DataTableRegion 5B88", "Unload 5B2A"] {
m.insert(k, Vec::new());
}
for i in 36..bytes.len().saturating_sub(1) {
if bytes[i] != 0x5B {
continue;
}
let k = match bytes[i + 1] {
0x20 => "Load 5B20",
0x1F => "LoadTable 5B1F",
0x88 => "DataTableRegion 5B88",
0x2A => "Unload 5B2A",
_ => continue,
};
m.get_mut(k).unwrap().push(i);
}
m
}
/// The nearest `Name (XXXX, Buffer (...))` before `at` (DefName and DefBuffer,
/// ACPI 6.5 §20.2.5) whose PkgLength covers `at`: its name and length.
fn in_named_buffer(b: &[u8], at: usize) -> Option<([u8; 4], usize)> {
let lead = |c: u8| c.is_ascii_uppercase() || c == b'_';
let tail = |c: u8| lead(c) || c.is_ascii_digit();
let pkglen = |p: usize| -> Option<(usize, usize)> {
let l = *b.get(p)?;
let n = (l >> 6) as usize;
if n == 0 {
return Some(((l & 0x3F) as usize, 1));
}
let mut v = (l & 0x0F) as usize;
for i in 0..n {
v |= (*b.get(p + 1 + i)? as usize) << (4 + 8 * i);
}
Some((v, n + 1))
};
for p in (36..at).rev() {
if b[p] == 0x08 && p + 6 < b.len() && lead(b[p + 1]) && b[p + 2..p + 5].iter().all(|c| tail(*c)) && b[p + 5] == 0x11 {
let (len, _) = pkglen(p + 6)?;
if p + 6 + len > at {
return Some((b[p + 1..p + 5].try_into().unwrap(), len));
}
}
}
None
}
fn find_all(hay: &[u8], needle: &[u8]) -> Vec<usize> {
if needle.is_empty() || hay.len() < needle.len() {
return Vec::new();
}
let first = needle[0];
let mut out = Vec::new();
let mut i = 0;
while i + needle.len() <= hay.len() {
if hay[i] == first && &hay[i..i + needle.len()] == needle {
out.push(i);
}
i += 1;
}
out
}
fn main() {
let mut argv = std::env::args().skip(1);
let dir = PathBuf::from(argv.next().expect("bench dump dir"));
let out = PathBuf::from(argv.next().expect("output dir"));
fs::create_dir_all(&out).unwrap();
let child = std::thread::Builder::new().stack_size(256 << 20).spawn(move || body(&dir, &out)).unwrap();
child.join().unwrap();
}
fn body(dir: &Path, out: &Path) {
let b = Bench::open(dir);
let mut local = String::new();
let mut sum = String::new();
// --- What the dump holds.
writeln!(sum, "## inputs").unwrap();
writeln!(sum, "definition blocks whole in tables/: {}", b.tables.len()).unwrap();
let headers: Vec<String> = fs::read_dir(dir.join("headers")).unwrap().map(|e| e.unwrap().file_name().into_string().unwrap()).collect();
let ssdt_headers = headers.iter().filter(|n| n.starts_with("SSDT")).count();
writeln!(sum, "SSDT headers in headers/: {ssdt_headers}; SSDTs whole in tables/: {}", b.tables.len() - 1).unwrap();
writeln!(sum, "NVS ranges in the map: {}; dumped with bytes: {}", b.map.iter().filter(|r| r.label == "ACPI Non-volatile Storage").count(), b.nvs.len()).unwrap();
for n in &b.nvs {
writeln!(local, "nvs dump base={:#x} len={:#x} class={}", n.base, n.bytes.len(), mem_class(&b.map, n.base, n.bytes.len() as u64)).unwrap();
writeln!(sum, "an NVS dump of {} bytes, wholly in a range the map types {}", n.bytes.len(), mem_class(&b.map, n.base, n.bytes.len() as u64)).unwrap();
}
// --- The load.
let mut h = b.host(Mode::Deny);
let (mut i, loaded) = b.load(&mut h);
writeln!(sum, "\n## load (unrecorded reads answered 0)").unwrap();
for (n, r) in &loaded {
writeln!(sum, "{n}: {}", match r { Ok(()) => "loaded".to_string(), Err(e) => format!("refused: {}", kind(e)) }).unwrap();
}
for (k, v) in tally(&h.recs) {
writeln!(sum, " load access: {k} x{v}").unwrap();
}
for r in &h.recs {
writeln!(local, "load access {r:?}").unwrap();
}
// --- The namespace.
let entries = walk(&i);
let paths: BTreeSet<String> = entries.iter().map(|(p, _, _)| p.clone()).collect();
let kind_of: BTreeMap<String, Kind> = entries.iter().map(|(p, _, k)| (p.clone(), *k)).collect();
let mut processors: Vec<String> = entries.iter().filter(|(_, _, k)| *k == Kind::Processor).map(|(p, _, _)| p.clone()).collect();
let mut hid_host = b.host(Mode::Deny);
for (p, _, k) in &entries {
if *k == Kind::Device && paths.contains(&format!("{p}._HID")) {
if let Ok(Value::String(s)) = i_eval_hid(&mut i, &mut hid_host, p) {
if s == b"ACPI0007" {
processors.push(p.clone());
}
}
}
}
writeln!(local, "_HID evaluations made {} accesses", hid_host.recs.len()).unwrap();
writeln!(sum, "\n## namespace").unwrap();
writeln!(sum, "objects: {}", entries.len()).unwrap();
writeln!(sum, "Processor objects: {}; ACPI0007 devices: {}", entries.iter().filter(|(_, _, k)| *k == Kind::Processor).count(), processors.len() - entries.iter().filter(|(_, _, k)| *k == Kind::Processor).count()).unwrap();
let mut defines: BTreeMap<&str, usize> = BTreeMap::new();
for p in &processors {
writeln!(local, "processor {p}").unwrap();
for m in ["_OSC", "_PDC", "_CST", "_LPI", "_CPC"] {
if paths.contains(&format!("{p}.{m}")) {
*defines.entry(m).or_insert(0) += 1;
}
}
}
for m in ["_OSC", "_PDC", "_CST", "_LPI", "_CPC"] {
writeln!(sum, "processors defining {m}: {}", defines.get(m).copied().unwrap_or(0)).unwrap();
}
let oscs: Vec<&String> = paths.iter().filter(|p| p.ends_with("._OSC")).collect();
writeln!(sum, "_OSC objects anywhere: {}; \\_SB_._OSC: {}", oscs.len(), paths.contains("\\_SB_._OSC")).unwrap();
for p in &oscs {
writeln!(local, "_OSC at {p} kind={:?}", kind_of[*p]).unwrap();
}
let pdcs = paths.iter().filter(|p| p.ends_with("._PDC")).count();
writeln!(sum, "_PDC objects anywhere: {pdcs}").unwrap();
// §6.5.1: \_SB._INI first; then the rest, parent before child (walk order).
let mut inis: Vec<String> = entries.iter().filter(|(p, _, k)| p.ends_with("._INI") && *k == Kind::Method).map(|(p, _, _)| p.clone()).collect();
if let Some(at) = inis.iter().position(|p| p == "\\_SB_._INI") {
let sb = inis.remove(at);
inis.insert(0, sb);
}
writeln!(sum, "_INI methods: {}; \\_SB_._INI: {}", inis.len(), paths.contains("\\_SB_._INI")).unwrap();
for p in &inis {
writeln!(local, "_INI at {p}").unwrap();
}
for mode in [Mode::Deny, Mode::Zero] {
let mut hh = b.host(mode);
let (mut ii, _) = b.load(&mut hh);
let mark = hh.recs.len();
let mut outcomes: BTreeMap<String, usize> = BTreeMap::new();
for p in &inis {
let before = hh.recs.len();
let r = ii.evaluate(&mut hh, p, &[]);
let k1 = k1_writes(&hh.recs[before..]).len();
let smi = hh.recs[before..].iter().filter(|x| x.write && x.class == "SMI_CMD").count();
writeln!(local, "[{mode:?}] _INI {p}: {:?}; accesses {}, SMI_CMD writes {smi}, K1-class writes {k1}", r.as_ref().map(|_| ()).map_err(kind), hh.recs.len() - before).unwrap();
if smi > 0 {
writeln!(sum, "[{mode:?}] one _INI ({}) writes SMI_CMD {smi} times and ends {}", if p.contains(".EC__.") || p.ends_with(".EC__._INI") { "the embedded controller device's" } else { "another device's" }, match &r { Ok(_) => "having run".to_string(), Err(e) => kind(e) }).unwrap();
}
*outcomes.entry(match r { Ok(_) => "ran".into(), Err(e) => kind(&e) }).or_insert(0) += 1;
}
writeln!(sum, "[{mode:?}] the _INIs in walk order: {outcomes:?}").unwrap();
for (k, v) in tally(&hh.recs[mark..]) {
writeln!(sum, " _INI access: {k} x{v}").unwrap();
}
}
// --- The load sites, by raw opcode bytes: no false negative.
writeln!(sum, "\n## load sites (raw byte scan of each block after its header)").unwrap();
for (n, bytes) in &b.tables {
let m = scan_ops(bytes);
let line: Vec<String> = m.iter().map(|(k, v)| format!("{k}: {}", v.len())).collect();
writeln!(sum, "{n}: {}", line.join(", ")).unwrap();
for (k, v) in &m {
for at in v {
let end = (*at + 16).min(bytes.len());
writeln!(local, "{n} {k} at {at:#x}: {:02x?}", &bytes[*at..end]).unwrap();
match in_named_buffer(bytes, *at) {
Some((name, len)) => {
writeln!(sum, " the {k} pair lies inside Name ({}, Buffer) of {len} bytes: data", String::from_utf8_lossy(&name)).unwrap()
}
None => writeln!(sum, " the {k} pair lies in no named Buffer: a candidate site").unwrap(),
}
}
}
}
// --- Linux's dynamic blocks.
writeln!(sum, "\n## Linux's run-time blocks (dynamic/)").unwrap();
let mut dyns: Vec<(u32, String)> = fs::read_dir(dir.join("dynamic"))
.unwrap()
.map(|e| e.unwrap().file_name().into_string().unwrap())
.map(|n| (n.trim_start_matches(|c: char| c.is_ascii_alphabetic()).parse().unwrap_or(0), n))
.collect();
dyns.sort();
writeln!(sum, "blocks: {}", dyns.len()).unwrap();
for (_, n) in &dyns {
let bytes = fs::read(dir.join("dynamic").join(n)).unwrap();
let len = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize;
let sum0 = bytes.iter().fold(0u8, |s, x| s.wrapping_add(*x));
let tid = String::from_utf8_lossy(&bytes[16..24]).trim_end_matches(['\0', ' ']).to_string();
let mut hits = Vec::new();
for d in &b.nvs {
for off in find_all(&d.bytes, &bytes) {
hits.push(d.base + off as u64);
}
}
let classes: Vec<&str> = hits.iter().map(|a| mem_class(&b.map, *a, bytes.len() as u64)).collect();
let ops = scan_ops(&bytes);
let ops_line: Vec<String> = ops.iter().map(|(k, v)| format!("{k}: {}", v.len())).collect();
writeln!(
sum,
"{} ({tid}): length {} (header {}), checksum {}, copies found in the NVS dump: {} {:?}; {}",
&bytes[..4].iter().map(|c| *c as char).collect::<String>(),
bytes.len(),
len,
if sum0 == 0 { "sums to zero" } else { "does NOT sum to zero" },
hits.len(),
classes,
ops_line.join(", ")
)
.unwrap();
for a in &hits {
writeln!(local, "dynamic {n} ({tid}) found at {a:#x}").unwrap();
}
}
// --- Whether the NVS names the blocks at all: their lengths as DWORDs, their strings.
if !dyns.is_empty() {
let lens: BTreeSet<u32> = dyns.iter().map(|(_, n)| fs::read(dir.join("dynamic").join(n)).unwrap().len() as u32).collect();
let mut length_dwords = 0usize;
let mut strings: BTreeMap<String, usize> = BTreeMap::new();
for d in &b.nvs {
for off in 0..d.bytes.len().saturating_sub(3) {
if lens.contains(&u32::from_le_bytes(d.bytes[off..off + 4].try_into().unwrap())) {
length_dwords += 1;
}
}
for (_, n) in &dyns {
let bytes = fs::read(dir.join("dynamic").join(n)).unwrap();
for s in [&bytes[10..16], &bytes[16..24]] {
let s: Vec<u8> = s.iter().copied().take_while(|c| *c != 0).collect::<Vec<_>>();
let s = String::from_utf8_lossy(&s).trim_end().to_string();
let c = find_all(&d.bytes, s.as_bytes()).len();
strings.insert(s, c);
}
}
strings.insert("SSDT".into(), find_all(&d.bytes, b"SSDT").len());
}
writeln!(sum, "DWORDs in the NVS dump equal to a block's length, at any byte offset: {length_dwords}").unwrap();
writeln!(sum, "occurrences in the NVS dump of each block's OEM ID, OEM table ID, and \"SSDT\": {strings:?}").unwrap();
}
// --- The DSDT, then Linux's run-time blocks in its order, as `Load` would put them.
if !dyns.is_empty() {
writeln!(sum, "\n## the DSDT, then Linux's run-time blocks loaded onto it in Linux's order (no static SSDT)").unwrap();
let mut hh = b.host(Mode::Zero);
let (mut ii, _) = b.load(&mut hh);
for (_, n) in &dyns {
let bytes = fs::read(dir.join("dynamic").join(n)).unwrap();
let tid = String::from_utf8_lossy(&bytes[16..24]).trim_end_matches(['\0', ' ']).to_string();
let r = match toyos_acpi::Table::open(bytes.as_slice(), 0, b"SSDT", 36) {
Ok(t) => ii.load(&mut hh, &t),
Err(e) => Err(Error::Table(Box::leak(format!("{e:?}").into_boxed_str()))),
};
writeln!(sum, "{tid}: {}", match r { Ok(()) => "loaded".to_string(), Err(e) => format!("refused: {}", kind(&e)) }).unwrap();
}
let ps: BTreeSet<String> = walk(&ii).into_iter().map(|(p, _, _)| p).collect();
for m in ["_OSC", "_PDC", "_CST", "_LPI", "_CPC", "_PSS", "_PCT", "_PSD"] {
let n = processors.iter().filter(|p| ps.contains(&format!("{p}.{m}"))).count();
writeln!(sum, "processors then defining {m}: {n} of {}", processors.len()).unwrap();
}
}
// --- \_SB._OSC and each processor's _OSC.
let mut targets: Vec<(String, &str, std::ops::RangeInclusive<u32>)> = Vec::new();
if paths.contains("\\_SB_._OSC") {
targets.push(("\\_SB_._OSC".into(), PLATFORM, 0..=22));
}
for p in &processors {
if paths.contains(&format!("{p}._OSC")) {
targets.push((format!("{p}._OSC"), INTEL, 0..=13));
}
}
writeln!(sum, "\n## _OSC runs").unwrap();
writeln!(sum, "targets: {}", targets.len()).unwrap();
for (target, u, bits) in &targets {
let label = if *u == PLATFORM { "\\_SB._OSC (platform UUID)".to_string() } else { "a processor's _OSC (Intel UUID)".to_string() };
writeln!(sum, "\n### {label}").unwrap();
let mut capsets: Vec<(String, u32)> = vec![("0".into(), 0)];
for bit in bits.clone() {
if *u == INTEL && bit == 10 {
continue;
}
capsets.push((format!("bit {bit}"), 1 << bit));
}
let mut all_k1 = 0usize;
let mut differs = 0usize;
let mut nvs_written: BTreeMap<(u64, u8), BTreeMap<u32, u64>> = BTreeMap::new();
for mode in [Mode::Deny, Mode::Zero] {
for (name, caps) in &capsets {
for query in [true, false] {
let a = args(u, &[if query { 1 } else { 0 }, *caps]);
let plain = run(&b, mode, &[], target, &a);
let with = run(&b, mode, &inis, target, &a);
let k1 = k1_writes(&plain.recs).len() + k1_writes(&with.recs).len();
all_k1 += k1;
let same = plain.recs == with.recs && format!("{:?}", plain.result) == format!("{:?}", with.result);
if !same {
differs += 1;
}
let q = if query { "query" } else { "commit" };
writeln!(
sum,
"[{mode:?}] caps {name}, {q}: {} | with _INIs first: {} | accesses {} / {} | K1-class writes {k1} | same with and without _INIs: {same} | notifies {} | slept {} ms, stalled {} us",
decode(&plain.result),
decode(&with.result),
plain.recs.len(),
with.recs.len(),
plain.notifies.len(),
plain.slept_ms,
plain.stalled_us
)
.unwrap();
for (k, v) in tally(&plain.recs) {
writeln!(sum, " access: {k} x{v}").unwrap();
}
if !same {
for (k, v) in tally(&with.recs) {
writeln!(sum, " access with _INIs first: {k} x{v}").unwrap();
}
}
for r in &plain.recs {
writeln!(local, "[{mode:?}] {target} caps {name} {q} plain {r:?}").unwrap();
if r.write && r.space == "SystemMemory" && r.class == "NVS" && !query && mode == Mode::Deny {
nvs_written.entry((r.at, r.width)).or_default().insert(*caps, r.value);
}
}
for r in &with.recs {
writeln!(local, "[{mode:?}] {target} caps {name} {q} with-inis {r:?}").unwrap();
}
for (p, res) in &with.inis {
writeln!(local, "[{mode:?}] ini {p}: {res}").unwrap();
}
}
}
}
writeln!(sum, "runs whose trace or result differ with _INIs first: {differs}; K1-class writes over all runs: {all_k1}").unwrap();
// Where the commit records its capabilities, and what Linux's run left there.
for ((at, w), by_caps) in &nvs_written {
let tracks = by_caps.iter().all(|(c, v)| (*v as u32) & *c == *c);
let mut linux = 0u64;
for d in &b.nvs {
if let Some(off) = at.checked_sub(d.base) {
if off + (*w as u64) <= d.bytes.len() as u64 {
for k in 0..*w as usize {
linux |= (d.bytes[off as usize + k] as u64) << (8 * k);
}
}
}
}
let distinct: BTreeSet<u64> = by_caps.values().copied().collect();
writeln!(
sum,
"commit writes an NVS field of {w} byte(s): {} distinct value(s) over the cap sets, each holding the caps passed: {tracks}; the dump (Linux's run) holds {linux:#x} there",
distinct.len()
)
.unwrap();
writeln!(local, "nvs field {at:#x} w{w}: by caps {by_caps:x?}; dump {linux:#x}").unwrap();
// The same commits on a dump whose field reads 0: what the firmware itself sets there.
let mut zeroed = Bench { dir: b.dir.clone(), map: b.map.clone(), ports: b.ports.clone(), nvs: b.nvs.clone(), tables: b.tables.clone() };
for d in &mut zeroed.nvs {
if let Some(off) = at.checked_sub(d.base) {
if off + (*w as u64) <= d.bytes.len() as u64 {
for k in 0..*w as usize {
d.bytes[off as usize + k] = 0;
}
}
}
}
for caps in by_caps.keys() {
let a = args(u, &[0, *caps]);
let r = run(&zeroed, Mode::Deny, &[], target, &a);
let set: Vec<u64> = r.recs.iter().filter(|x| x.write && x.at == *at).map(|x| x.value).collect();
writeln!(sum, "with that field read as 0, the commit of caps {caps:#x} writes {set:#x?} there; the dump's value ANDed with it: {:#x?}", set.iter().map(|v| v & linux).collect::<Vec<_>>()).unwrap();
}
}
if *u == INTEL {
break; // one processor stands for the rest here; every processor's _OSC is listed in local.txt
}
}
let _ = &b.dir;
fs::write(out.join("local.txt"), local).unwrap();
fs::write(out.join("summary.txt"), sum).unwrap();
}
fn i_eval_hid(i: &mut Interpreter, h: &mut Tracer, p: &str) -> Result<Value, Error> {
i.evaluate(h, &format!("{p}._HID"), &[])
} |
|
M1's result: K1–K5 against the dumps as they were taken. The logs are posted whole: part 1, the T14, part 2, the AMD bench and part 3, the harness. They hold counts and decoded values only. The dumps hold neither bench's static SSDTsStep 1 of the request runs
M1 therefore ran on each DSDT, and on the T14 also with Linux's 8 run-time blocks loaded onto it. Where a condition needs what the SSDTs hold, it is undecided. How it ran
K1–K5K1 (
K2 (
K3 (a
K4 (a bench's processors define
K5 (the AMD's processors have no
Its 16 SSDTs, which hold For the staging, beyond K1–K5
To finish M1
The harness reruns unchanged on a full dump. |
|
M1 on complete tables, part 1 of 6: the T14's log, lines 1– 374 of 635. |
|
M1 on complete tables, part 2 of 6: the T14's log, lines 375– 635 of 635. The rest of part 1's |
|
M1 on complete tables, part 3 of 6: the AMD bench's log, whole. |
|
M1 on complete tables, part 4 of 6: the unchanged harness's T14 log, whole. |
|
M1 on complete tables, part 5 of 6: the unchanged harness's AMD log, whole. |
|
M1 on complete tables, part 6 of 6: the extended harness and the runs' log. The source is the first run's with the diff below applied; The runs' log, whole: The diff, whole: --- first-run/src/main.rs
+++ m1b/src/main.rs
@@ -7,6 +7,24 @@
//! dump (an overlay that the run's own writes update); every other read is
//! unrecorded and is refused (`deny`) or answered 0 (`zero`). Every write is
//! accepted and recorded.
+//!
+//! M1b adds, behind the third argument `bridges` (any other value or none
+//! runs the first run's harness unchanged):
+//! - the interpreter's walk to a PCI_Config region below a bridge (Header
+//! Type at 0x0E, Secondary Bus Number at 0x19, one byte each) is answered
+//! as a PCI-to-PCI bridge whose secondary bus is one above its own, since
+//! the dump holds no configuration space; every such answer is counted;
+//! - each table's refusal in full;
+//! - each `Load` site's source region, decoded from the bytes, and the
+//! address and length it names, typed by the memory map (K3);
+//! - `\_SB._OSC` then each processor's `_OSC`, query then commit, in one
+//! namespace, on copies of the tables whose every `Load (Src, Dst)` is
+//! rewritten, same length and checksum, to `Store (Ones, Dst)` and Noops,
+//! so a run continues past it; a `Dst` set after a commit loads Linux's
+//! run-time block that the source names, as `Load` would have;
+//! - which bytes an `_OSC` run reads that an `_INI` writes (K2's channel
+//! through memory, which a dump taken after Linux's `_INI`s hides), and
+//! whether forcing them to 0x00 or 0xFF changes the run.
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;
@@ -145,6 +163,7 @@
slept_ms: u64,
stalled_us: u64,
t: u64,
+ bridges: bool,
}
fn bytes_of(w: Access) -> u8 {
@@ -157,8 +176,8 @@
}
impl Tracer {
- fn new(map: &[Range], ports: &[(u64, u64, &'static str)], nvs: &[Nvs], mode: Mode) -> Self {
- Tracer { map: map.to_vec(), ports: ports.to_vec(), overlay: nvs.to_vec(), mode, recs: Vec::new(), notifies: Vec::new(), slept_ms: 0, stalled_us: 0, t: 0 }
+ fn new(map: &[Range], ports: &[(u64, u64, &'static str)], nvs: &[Nvs], mode: Mode, bridges: bool) -> Self {
+ Tracer { map: map.to_vec(), ports: ports.to_vec(), overlay: nvs.to_vec(), mode, recs: Vec::new(), notifies: Vec::new(), slept_ms: 0, stalled_us: 0, t: 0, bridges }
}
fn port_class(&self, p: u16, n: u8) -> String {
@@ -196,6 +215,11 @@
fn read(&mut self, at: Address, width: Access) -> Result<u64, Denied> {
let n = bytes_of(width);
let (space, addr, class) = self.classify(at, n);
+ if let (true, Address::PciConfig { bus, offset: off @ (0x0E | 0x19), .. }, Access::Byte) = (self.bridges, at, width) {
+ let v = if off == 0x0E { 0x01 } else { bus as u64 + 1 };
+ self.recs.push(Rec { space, write: false, width: n, at: addr, class, value: v, answered: "as a bridge" });
+ return Ok(v);
+ }
if let Address::Memory(a) = at {
if let Some((i, off)) = self.in_nvs(a, n) {
let mut v = 0u64;
@@ -275,12 +299,14 @@
const PLATFORM: &str = "0811B06E-4A27-44F9-8D60-3CBBC22E7B48";
const INTEL: &str = "4077A616-290C-47BE-9EBD-D87058713953";
+#[derive(Clone)]
struct Bench {
dir: PathBuf,
map: Vec<Range>,
ports: Vec<(u64, u64, &'static str)>,
nvs: Vec<Nvs>,
tables: Vec<(String, Vec<u8>)>,
+ bridges: bool,
}
fn tables(dir: &Path) -> Vec<(String, Vec<u8>)> {
@@ -316,12 +342,12 @@
}
impl Bench {
- fn open(dir: &Path) -> Bench {
- Bench { dir: dir.to_path_buf(), map: memmap(dir), ports: fadt_ports(dir), nvs: nvs_dumps(dir), tables: tables(dir) }
+ fn open(dir: &Path, bridges: bool) -> Bench {
+ Bench { dir: dir.to_path_buf(), map: memmap(dir), ports: fadt_ports(dir), nvs: nvs_dumps(dir), tables: tables(dir), bridges }
}
fn host(&self, mode: Mode) -> Tracer {
- Tracer::new(&self.map, &self.ports, &self.nvs, mode)
+ Tracer::new(&self.map, &self.ports, &self.nvs, mode, self.bridges)
}
/// Every table loaded into a fresh interpreter, in order; each one's result.
@@ -503,13 +529,14 @@
let mut argv = std::env::args().skip(1);
let dir = PathBuf::from(argv.next().expect("bench dump dir"));
let out = PathBuf::from(argv.next().expect("output dir"));
+ let extended = argv.next().as_deref() == Some("bridges");
fs::create_dir_all(&out).unwrap();
- let child = std::thread::Builder::new().stack_size(256 << 20).spawn(move || body(&dir, &out)).unwrap();
+ let child = std::thread::Builder::new().stack_size(256 << 20).spawn(move || body(&dir, &out, extended)).unwrap();
child.join().unwrap();
}
-fn body(dir: &Path, out: &Path) {
- let b = Bench::open(dir);
+fn body(dir: &Path, out: &Path, extended: bool) {
+ let b = Bench::open(dir, extended);
let mut local = String::new();
let mut sum = String::new();
@@ -520,6 +547,23 @@
let ssdt_headers = headers.iter().filter(|n| n.starts_with("SSDT")).count();
writeln!(sum, "SSDT headers in headers/: {ssdt_headers}; SSDTs whole in tables/: {}", b.tables.len() - 1).unwrap();
writeln!(sum, "NVS ranges in the map: {}; dumped with bytes: {}", b.map.iter().filter(|r| r.label == "ACPI Non-volatile Storage").count(), b.nvs.len()).unwrap();
+ if extended {
+ // Each whole table against the header the dump took of it separately.
+ for (n, bytes) in &b.tables {
+ let header = fs::read(dir.join("headers").join(n)).unwrap_or_default();
+ let len = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize;
+ let sum0 = bytes.iter().fold(0u8, |s, x| s.wrapping_add(*x));
+ writeln!(
+ sum,
+ "{n}: {} bytes; header's length {}; checksum {}; first 36 bytes equal headers/{n}: {}",
+ bytes.len(),
+ len,
+ if sum0 == 0 { "sums to zero" } else { "does NOT sum to zero" },
+ header.len() == 36 && bytes.len() >= 36 && bytes[..36] == header[..]
+ )
+ .unwrap();
+ }
+ }
for n in &b.nvs {
writeln!(local, "nvs dump base={:#x} len={:#x} class={}", n.base, n.bytes.len(), mem_class(&b.map, n.base, n.bytes.len() as u64)).unwrap();
writeln!(sum, "an NVS dump of {} bytes, wholly in a range the map types {}", n.bytes.len(), mem_class(&b.map, n.base, n.bytes.len() as u64)).unwrap();
@@ -531,6 +575,9 @@
writeln!(sum, "\n## load (unrecorded reads answered 0)").unwrap();
for (n, r) in &loaded {
writeln!(sum, "{n}: {}", match r { Ok(()) => "loaded".to_string(), Err(e) => format!("refused: {}", kind(e)) }).unwrap();
+ if let (true, Err(e)) = (extended, r) {
+ writeln!(sum, " {n} refused in full: {e:?}").unwrap();
+ }
}
for (k, v) in tally(&h.recs) {
writeln!(sum, " load access: {k} x{v}").unwrap();
@@ -697,9 +744,14 @@
// --- The DSDT, then Linux's run-time blocks in its order, as `Load` would put them.
if !dyns.is_empty() {
- writeln!(sum, "\n## the DSDT, then Linux's run-time blocks loaded onto it in Linux's order (no static SSDT)").unwrap();
+ if extended {
+ writeln!(sum, "\n## every table in tables/, then Linux's run-time blocks loaded onto them in Linux's order").unwrap();
+ } else {
+ writeln!(sum, "\n## the DSDT, then Linux's run-time blocks loaded onto it in Linux's order (no static SSDT)").unwrap();
+ }
let mut hh = b.host(Mode::Zero);
let (mut ii, _) = b.load(&mut hh);
+ let before_dyn = hh.recs.len();
for (_, n) in &dyns {
let bytes = fs::read(dir.join("dynamic").join(n)).unwrap();
let tid = String::from_utf8_lossy(&bytes[16..24]).trim_end_matches(['\0', ' ']).to_string();
@@ -709,6 +761,12 @@
};
writeln!(sum, "{tid}: {}", match r { Ok(()) => "loaded".to_string(), Err(e) => format!("refused: {}", kind(&e)) }).unwrap();
}
+ if extended {
+ writeln!(sum, "accesses while the run-time blocks loaded: {}", hh.recs.len() - before_dyn).unwrap();
+ for (k, v) in tally(&hh.recs[before_dyn..]) {
+ writeln!(sum, " run-time block load access: {k} x{v}").unwrap();
+ }
+ }
let ps: BTreeSet<String> = walk(&ii).into_iter().map(|(p, _, _)| p).collect();
for m in ["_OSC", "_PDC", "_CST", "_LPI", "_CPC", "_PSS", "_PCT", "_PSD"] {
let n = processors.iter().filter(|p| ps.contains(&format!("{p}.{m}"))).count();
@@ -812,7 +870,7 @@
.unwrap();
writeln!(local, "nvs field {at:#x} w{w}: by caps {by_caps:x?}; dump {linux:#x}").unwrap();
// The same commits on a dump whose field reads 0: what the firmware itself sets there.
- let mut zeroed = Bench { dir: b.dir.clone(), map: b.map.clone(), ports: b.ports.clone(), nvs: b.nvs.clone(), tables: b.tables.clone() };
+ let mut zeroed = b.clone();
for d in &mut zeroed.nvs {
if let Some(off) = at.checked_sub(d.base) {
if off + (*w as u64) <= d.bytes.len() as u64 {
@@ -833,6 +891,9 @@
break; // one processor stands for the rest here; every processor's _OSC is listed in local.txt
}
}
+ if extended {
+ m1b(&b, dir, &processors, &inis, &mut sum, &mut local);
+ }
let _ = &b.dir;
fs::write(out.join("local.txt"), local).unwrap();
fs::write(out.join("summary.txt"), sum).unwrap();
@@ -840,4 +901,580 @@
fn i_eval_hid(i: &mut Interpreter, h: &mut Tracer, p: &str) -> Result<Value, Error> {
i.evaluate(h, &format!("{p}._HID"), &[])
+}
+
+// ---------------------------------------------------------------- M1b
+
+/// A NameString (ACPI 6.5 §20.2.2) at `i`: its text, segments joined by
+/// dots after any `\` or `^` prefix, and the offset after it.
+fn name_string(b: &[u8], mut i: usize) -> Option<(String, usize)> {
+ let mut s = String::new();
+ while let Some(&c) = b.get(i) {
+ if c == b'\\' || c == b'^' {
+ s.push(c as char);
+ i += 1;
+ } else {
+ break;
+ }
+ }
+ let seg = |i: usize| -> Option<String> {
+ let x = b.get(i..i + 4)?;
+ let lead = x[0].is_ascii_uppercase() || x[0] == b'_';
+ let tail = x[1..].iter().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || *c == b'_');
+ (lead && tail).then(|| String::from_utf8_lossy(x).into_owned())
+ };
+ match *b.get(i)? {
+ 0x00 => Some((s, i + 1)),
+ 0x2E => {
+ let (a, c) = (seg(i + 1)?, seg(i + 5)?);
+ Some((format!("{s}{a}.{c}"), i + 9))
+ }
+ 0x2F => {
+ let n = *b.get(i + 1)? as usize;
+ let mut j = i + 2;
+ let mut segs = Vec::new();
+ for _ in 0..n {
+ segs.push(seg(j)?);
+ j += 4;
+ }
+ Some((format!("{s}{}", segs.join(".")), j))
+ }
+ _ => Some((format!("{s}{}", seg(i)?), i + 4)),
+ }
+}
+
+fn last_seg(n: &str) -> &str {
+ n.rsplit('.').next().unwrap().trim_start_matches(['\\', '^'])
+}
+
+/// A constant integer term (§20.2.3): Zero, One, Ones, or a prefixed constant.
+fn const_int(b: &[u8], i: usize) -> Option<(u64, usize)> {
+ match *b.get(i)? {
+ 0x00 => Some((0, i + 1)),
+ 0x01 => Some((1, i + 1)),
+ 0xFF => Some((u64::MAX, i + 1)),
+ 0x0A => Some((*b.get(i + 1)? as u64, i + 2)),
+ 0x0B => Some((u16::from_le_bytes(b.get(i + 1..i + 3)?.try_into().ok()?) as u64, i + 3)),
+ 0x0C => Some((u32::from_le_bytes(b.get(i + 1..i + 5)?.try_into().ok()?) as u64, i + 5)),
+ 0x0E => Some((u64::from_le_bytes(b.get(i + 1..i + 9)?.try_into().ok()?), i + 9)),
+ _ => None,
+ }
+}
+
+/// A region's offset or length operand, as far as this reads it:
+/// `DerefOf (Index (Name, k))` with no target, a constant, or a name.
+#[derive(Clone, Debug)]
+enum Operand {
+ Elem(String, u64),
+ Const(u64),
+ Name(String),
+}
+
+fn operand(b: &[u8], i: usize) -> Option<(Operand, usize)> {
+ if b.get(i) == Some(&0x83) && b.get(i + 1) == Some(&0x88) {
+ let (n, j) = name_string(b, i + 2)?;
+ let (k, j) = const_int(b, j)?;
+ if b.get(j) != Some(&0x00) {
+ return None;
+ }
+ return Some((Operand::Elem(n, k), j + 1));
+ }
+ if let Some((v, j)) = const_int(b, i) {
+ return Some((Operand::Const(v), j));
+ }
+ let (n, j) = name_string(b, i)?;
+ Some((Operand::Name(n), j))
+}
+
+/// One `Load (Src, Dst)` (DefLoad, Table 20.2) and the OperationRegion
+/// (DefOpRegion) nearest before it that declares `Src`.
+struct Site {
+ table: String,
+ at: usize,
+ src: String,
+ dst: String,
+ end: usize,
+ region: Option<(u8, Operand, Operand)>,
}
+
+fn load_sites(tables: &[(String, Vec<u8>)]) -> Vec<Site> {
+ let mut out = Vec::new();
+ for (n, b) in tables {
+ let ops = scan_ops(b);
+ for &at in &ops["Load 5B20"] {
+ if in_named_buffer(b, at).is_some() {
+ continue;
+ }
+ let Some((src, j)) = name_string(b, at + 2) else { continue };
+ let Some((dst, end)) = name_string(b, j) else { continue };
+ let mut region = None;
+ for p in (36..at).rev() {
+ if b[p] != 0x5B || b[p + 1] != 0x80 {
+ continue;
+ }
+ let Some((rn, q)) = name_string(b, p + 2) else { continue };
+ if last_seg(&rn) != last_seg(&src) {
+ continue;
+ }
+ if let Some((off, q2)) = operand(b, q + 1) {
+ if let Some((len, _)) = operand(b, q2) {
+ region = Some((b[q], off, len));
+ }
+ }
+ break;
+ }
+ out.push(Site { table: n.clone(), at, src, dst, end, region });
+ }
+ }
+ out
+}
+
+/// The tables with every site's `Load (Src, Dst)` rewritten in place to
+/// `Store (Ones, Dst)` and Noops, of the same length, the checksum (§5.2.6)
+/// made whole again.
+fn patched(tables: &[(String, Vec<u8>)], sites: &[Site]) -> Vec<(String, Vec<u8>)> {
+ let mut out = tables.to_vec();
+ for s in sites {
+ let (_, b) = out.iter_mut().find(|(n, _)| *n == s.table).unwrap();
+ let src_end = name_string(b, s.at + 2).unwrap().1;
+ let mut new = vec![0x70, 0xFF];
+ new.extend_from_slice(&b[src_end..s.end]);
+ new.extend(std::iter::repeat_n(0xA3u8, src_end - (s.at + 2)));
+ assert_eq!(new.len(), s.end - s.at);
+ b[s.at..s.end].copy_from_slice(&new);
+ }
+ for (_, b) in &mut out {
+ b[9] = 0;
+ let sum = b.iter().fold(0u8, |s, x| s.wrapping_add(*x));
+ b[9] = 0u8.wrapping_sub(sum);
+ }
+ out
+}
+
+/// What a site's region names, read from the loaded namespace: the label,
+/// address and length of the package elements it indexes.
+struct Source {
+ label: String,
+ at: u64,
+ len: u64,
+}
+
+fn resolve(paths: &BTreeSet<String>, name: &str) -> Vec<String> {
+ if name.starts_with('\\') {
+ let p = name.replace('.', ".");
+ return paths.iter().filter(|x| **x == p).cloned().collect();
+ }
+ let seg = last_seg(name);
+ let root = format!("\\{seg}");
+ if paths.contains(&root) {
+ return vec![root];
+ }
+ paths.iter().filter(|p| p.ends_with(&format!(".{seg}"))).cloned().collect()
+}
+
+fn source_of(i: &mut Interpreter, h: &mut Tracer, paths: &BTreeSet<String>, s: &Site) -> Result<Source, String> {
+ let Some((space, off, len)) = &s.region else { return Err("no region decoded".into()) };
+ if *space != 0 {
+ return Err(format!("region space {space:#x}, not SystemMemory"));
+ }
+ let mut elem = |o: &Operand| -> Result<(u64, Option<String>), String> {
+ match o {
+ Operand::Const(v) => Ok((*v, None)),
+ Operand::Name(n) => {
+ let p = resolve(paths, n);
+ if p.len() != 1 {
+ return Err(format!("{n} resolves to {} objects", p.len()));
+ }
+ match i.evaluate(h, &p[0], &[]) {
+ Ok(Value::Integer(v)) => Ok((v, None)),
+ other => Err(format!("{n} evaluates to {}", decode(&other))),
+ }
+ }
+ Operand::Elem(n, k) => {
+ let p = resolve(paths, n);
+ if p.len() != 1 {
+ return Err(format!("{n} resolves to {} objects", p.len()));
+ }
+ match i.evaluate(h, &p[0], &[]) {
+ Ok(Value::Package(e)) => {
+ let v = match e.get(*k as usize) {
+ Some(Value::Integer(v)) => *v,
+ _ => return Err(format!("{n}[{k}] is no integer")),
+ };
+ let label = match (*k as usize).checked_sub(1).and_then(|j| e.get(j)) {
+ Some(Value::String(t)) => Some(String::from_utf8_lossy(t).trim().to_string()),
+ _ => None,
+ };
+ Ok((v, label))
+ }
+ other => Err(format!("{n} evaluates to {}", decode(&other))),
+ }
+ }
+ }
+ };
+ let (at, label) = elem(off)?;
+ let (len, _) = elem(len)?;
+ Ok(Source { label: label.unwrap_or_else(|| "(unlabelled)".into()), at, len })
+}
+
+fn show(o: &Operand) -> String {
+ match o {
+ Operand::Elem(n, k) => format!("DerefOf ({n} [{k}])"),
+ Operand::Const(_) => "a constant".into(),
+ Operand::Name(n) => n.clone(),
+ }
+}
+
+fn dword2(r: &Result<Value, Error>) -> Option<u32> {
+ match r {
+ Ok(Value::Buffer(b)) if b.len() == 8 => Some(u32::from_le_bytes(b[4..8].try_into().unwrap())),
+ _ => None,
+ }
+}
+
+#[derive(PartialEq)]
+struct SeqOut {
+ steps: Vec<(String, String)>,
+ reached: Vec<(String, String)>,
+ blocks: Vec<(String, String)>,
+ recs: Vec<Rec>,
+ block_recs: usize,
+ inis: Vec<(String, String)>,
+ /// What the platform commit left in `\_SB.OSCP`, where a table defines one: for local.txt only.
+ notes: Vec<String>,
+}
+
+struct Plan<'a> {
+ bench: Bench,
+ processors: Vec<String>,
+ platform: bool,
+ /// Each site's Dst path in the namespace, and the label of the block its source names.
+ dsts: Vec<(String, String)>,
+ blocks: &'a BTreeMap<String, Vec<u8>>,
+}
+
+fn sequence(plan: &Plan, mode: Mode, inis: &[String], pcaps: u32, ccaps: u32, force: &[(u64, u8)]) -> SeqOut {
+ let mut h = plan.bench.host(mode);
+ for (a, v) in force {
+ for d in &mut h.overlay {
+ if let Some(off) = a.checked_sub(d.base) {
+ if (off as usize) < d.bytes.len() {
+ d.bytes[off as usize] = *v;
+ }
+ }
+ }
+ }
+ let (mut i, _) = plan.bench.load(&mut h);
+ let mut ini_out = Vec::new();
+ for p in inis {
+ let r = i.evaluate(&mut h, p, &[]);
+ ini_out.push((p.clone(), match r { Ok(_) => "ran".into(), Err(e) => kind(&e) }));
+ }
+ let mark = h.recs.len();
+ let mut steps = Vec::new();
+ let mut reached: Vec<(String, String)> = Vec::new();
+ let mut blocks = Vec::new();
+ let mut block_recs = 0usize;
+ let mut done: BTreeSet<String> = BTreeSet::new();
+ let mut notes = Vec::new();
+ let mut check = |i: &mut Interpreter, h: &mut Tracer, who: &str, reached: &mut Vec<(String, String)>, blocks: &mut Vec<(String, String)>, block_recs: &mut usize| {
+ for (dst, label) in &plan.dsts {
+ if done.contains(dst) {
+ continue;
+ }
+ if let Ok(Value::Integer(v)) = i.evaluate(h, dst, &[]) {
+ if v != 0 {
+ done.insert(dst.clone());
+ reached.push((who.to_string(), label.clone()));
+ let before = h.recs.len();
+ let r = match plan.blocks.get(&label.to_ascii_uppercase()) {
+ Some(bytes) => match toyos_acpi::Table::open(bytes.as_slice(), 0, b"SSDT", 36) {
+ Ok(t) => match i.load(h, &t) {
+ Ok(()) => "loaded".to_string(),
+ Err(e) => format!("refused: {}", kind(&e)),
+ },
+ Err(e) => format!("refused: {e:?}"),
+ },
+ None => "no block of that OEM table ID in dynamic/".to_string(),
+ };
+ *block_recs += h.recs.len() - before;
+ blocks.push((label.clone(), r));
+ }
+ }
+ }
+ };
+ if plan.platform {
+ let q = i.evaluate(&mut h, "\\_SB_._OSC", &args(PLATFORM, &[1, pcaps]));
+ let granted = dword2(&q).unwrap_or(0) & pcaps;
+ let c = i.evaluate(&mut h, "\\_SB_._OSC", &args(PLATFORM, &[0, granted]));
+ steps.push(("platform query".to_string(), decode(&q)));
+ steps.push(("platform commit".to_string(), decode(&c)));
+ check(&mut i, &mut h, "\\_SB._OSC", &mut reached, &mut blocks, &mut block_recs);
+ if let Ok(v) = i.evaluate(&mut h, "\\_SB_.OSCP", &[]) {
+ notes.push(format!("\\_SB.OSCP after the platform commit: {v:x?}"));
+ }
+ }
+ for p in &plan.processors {
+ let path = format!("{p}._OSC");
+ let q = i.evaluate(&mut h, &path, &args(INTEL, &[1, ccaps]));
+ let granted = dword2(&q).unwrap_or(0) & ccaps;
+ let c = i.evaluate(&mut h, &path, &args(INTEL, &[0, granted]));
+ steps.push((format!("{p} query"), decode(&q)));
+ steps.push((format!("{p} commit"), decode(&c)));
+ check(&mut i, &mut h, p, &mut reached, &mut blocks, &mut block_recs);
+ }
+ SeqOut { steps, reached, blocks, recs: h.recs[mark..].to_vec(), block_recs, inis: ini_out, notes }
+}
+
+/// Byte addresses a record covers, in SystemMemory only.
+fn mem_bytes(r: &Rec) -> Option<std::ops::Range<u64>> {
+ (r.space == "SystemMemory").then(|| r.at..r.at + r.width as u64)
+}
+
+fn m1b(b: &Bench, dir: &Path, processors: &[String], inis: &[String], sum: &mut String, local: &mut String) {
+ // --- K3: each Load site's source.
+ writeln!(sum, "\n## M1b: each Load site's source (K3)").unwrap();
+ let sites = load_sites(&b.tables);
+ writeln!(sum, "Load sites outside named Buffers, over every table: {}", sites.len()).unwrap();
+ let mut h = b.host(Mode::Deny);
+ let (mut i, _) = b.load(&mut h);
+ let paths: BTreeSet<String> = walk(&i).into_iter().map(|(p, _, _)| p).collect();
+ let mut dyn_blocks: BTreeMap<String, Vec<u8>> = BTreeMap::new();
+ if let Ok(rd) = fs::read_dir(dir.join("dynamic")) {
+ for e in rd {
+ let bytes = fs::read(e.unwrap().path()).unwrap();
+ let tid = String::from_utf8_lossy(&bytes[16..24]).trim_end_matches(['\0', ' ']).to_ascii_uppercase();
+ dyn_blocks.insert(tid, bytes);
+ }
+ }
+ let mut dsts = Vec::new();
+ let mut named_elems: BTreeSet<(String, u64)> = BTreeSet::new();
+ for s in &sites {
+ let reg = match &s.region {
+ Some((space, off, len)) => format!("region space {space:#x}, offset {}, length {}", show(off), show(len)),
+ None => "no OperationRegion of that name decoded before it".into(),
+ };
+ if let Some((_, Operand::Elem(n, k), Operand::Elem(_, k2))) = &s.region {
+ named_elems.insert((last_seg(n).to_string(), *k));
+ named_elems.insert((last_seg(n).to_string(), *k2));
+ if let Some(kl) = k.checked_sub(1) {
+ named_elems.insert((last_seg(n).to_string(), kl));
+ }
+ }
+ let src = source_of(&mut i, &mut h, &paths, s);
+ let line = match &src {
+ Ok(x) => {
+ let class = mem_class(&b.map, x.at, x.len);
+ let linux = match dyn_blocks.get(&x.label.to_ascii_uppercase()) {
+ Some(d) => format!("Linux's run-time block of that OEM table ID: {} bytes, equal to the region's length: {}", d.len(), d.len() as u64 == x.len),
+ None => "no run-time block of that OEM table ID in dynamic/".into(),
+ };
+ writeln!(local, "site {} {:#x}: {} at {:#x} len {:#x}", s.table, s.at, x.label, x.at, x.len).unwrap();
+ format!("names {} of {} bytes, wholly in a range the map types {class}; {linux}", x.label, x.len)
+ }
+ Err(e) => format!("source unresolved: {e}"),
+ };
+ writeln!(sum, "{}: Load ({}, {}): {reg}; {line}", s.table, s.src, s.dst).unwrap();
+ let label = src.map(|x| x.label).unwrap_or_default();
+ for p in paths.iter().filter(|p| last_seg(p) == last_seg(&s.dst) && (p.ends_with(&format!(".{}", last_seg(&s.dst))) || **p == format!("\\{}", last_seg(&s.dst)))) {
+ dsts.push((p.clone(), label.clone()));
+ }
+ }
+ // Package entries that look like (label, address, length) and no site names.
+ let pkgs: BTreeSet<String> = named_elems.iter().map(|(n, _)| n.clone()).collect();
+ for n in &pkgs {
+ for p in resolve(&paths, n) {
+ if let Ok(Value::Package(e)) = i.evaluate(&mut h, &p, &[]) {
+ for k in 0..e.len() {
+ if let (Some(Value::String(t)), Some(Value::Integer(at)), Some(Value::Integer(len))) = (e.get(k), e.get(k + 1), e.get(k + 2)) {
+ if !named_elems.contains(&(n.clone(), k as u64 + 1)) {
+ writeln!(sum, "{p}: entry {} of {len:#x} bytes is named by no Load site; its address lies in a range the map types {}", String::from_utf8_lossy(t).trim(), mem_class(&b.map, *at, *len)).unwrap();
+ }
+ }
+ }
+ }
+ }
+ }
+ writeln!(sum, "Dst objects the rewritten tables set: {}", dsts.len()).unwrap();
+ for (d, l) in &dsts {
+ writeln!(sum, " {d} <- {l}").unwrap();
+ }
+
+ // --- The _OSC sequences, past each Load.
+ let pb = Bench { tables: patched(&b.tables, &sites), ..b.clone() };
+ let procs: Vec<String> = processors.iter().filter(|p| paths.contains(&format!("{p}._OSC"))).cloned().collect();
+ let plan = Plan { bench: pb, processors: procs.clone(), platform: paths.contains("\\_SB_._OSC"), dsts, blocks: &dyn_blocks };
+ writeln!(sum, "\n## M1b: \\_SB._OSC, then each processor's _OSC in walk order ({}), query then commit with what the query granted, in one namespace, every Load rewritten", procs.len()).unwrap();
+ {
+ let mut hp = plan.bench.host(Mode::Deny);
+ let (_, loaded) = plan.bench.load(&mut hp);
+ let refused: Vec<String> = loaded.iter().filter_map(|(n, r)| r.as_ref().err().map(|e| format!("{n}: {e:?}"))).collect();
+ writeln!(sum, "the rewritten tables load with refusals: {refused:?}").unwrap();
+ }
+ let mut sets: Vec<(String, u32, u32)> = vec![("S1: platform 0, processors 0".into(), 0, 0)];
+ if !procs.is_empty() {
+ for bit in 0..=13u32 {
+ if bit != 10 {
+ sets.push((format!("processors bit {bit}"), 0, 1 << bit));
+ }
+ }
+ }
+ if plan.platform {
+ for bit in 0..=22u32 {
+ sets.push((format!("platform bit {bit}"), 1 << bit, 0));
+ }
+ }
+ sets.push(("every bit: platform 0x7FFFFF, processors 0x3BFF".into(), 0x7F_FFFF, 0x3BFF));
+ let mut read_bytes: BTreeMap<u64, BTreeSet<String>> = BTreeMap::new();
+ let mut all_k1 = 0usize;
+ let mut differs = 0usize;
+ for mode in [Mode::Deny, Mode::Zero] {
+ for (name, pc, cc) in &sets {
+ let plain = sequence(&plan, mode, &[], *pc, *cc, &[]);
+ let with = sequence(&plan, mode, inis, *pc, *cc, &[]);
+ let k1 = k1_writes(&plain.recs).len() + k1_writes(&with.recs).len();
+ all_k1 += k1;
+ let same = plain.steps == with.steps && plain.reached == with.reached && plain.blocks == with.blocks && plain.recs == with.recs;
+ if !same {
+ differs += 1;
+ }
+ for r in plain.recs.iter().filter(|r| !r.write) {
+ if let Some(rg) = mem_bytes(r) {
+ for a in rg {
+ read_bytes.entry(a).or_default().insert(name.clone());
+ }
+ }
+ }
+ let mut by: BTreeMap<String, usize> = BTreeMap::new();
+ for (w, d) in &plain.steps {
+ if w.starts_with("\\_SB_.PR") || (w != "platform query" && w != "platform commit") {
+ *by.entry(format!("{} {d}", if w.ends_with("query") { "query" } else { "commit" })).or_insert(0) += 1;
+ }
+ }
+ let plat: Vec<String> = plain.steps.iter().filter(|(w, _)| w.starts_with("platform")).map(|(w, d)| format!("{w}: {d}")).collect();
+ let reached: Vec<String> = plain.reached.iter().map(|(w, l)| format!("{l} (by {})", if w.starts_with("\\_SB_.PR") || w.starts_with("\\_PR_") { if w == &procs[0] { "the first processor" } else { "a later processor" } } else { w.as_str() })).collect();
+ let blocks: Vec<String> = plain.blocks.iter().map(|(l, r)| format!("{l}: {r}")).collect();
+ writeln!(
+ sum,
+ "[{mode:?}] {name} | {} | processors: {by:?} | Loads reached: {reached:?} | blocks then loaded: {blocks:?} ({} accesses) | accesses {} / {} with _INIs first | K1-class writes {k1} | same with and without _INIs: {same}",
+ plat.join("; "),
+ plain.block_recs,
+ plain.recs.len(),
+ with.recs.len()
+ )
+ .unwrap();
+ for (k, v) in tally(&plain.recs) {
+ writeln!(sum, " access: {k} x{v}").unwrap();
+ }
+ if !same {
+ let wr: Vec<String> = with.reached.iter().map(|(_, l)| l.clone()).collect();
+ writeln!(sum, " with _INIs first: Loads reached {wr:?}").unwrap();
+ for (k, v) in tally(&with.recs) {
+ writeln!(sum, " access with _INIs first: {k} x{v}").unwrap();
+ }
+ }
+ for r in &plain.recs {
+ writeln!(local, "[{mode:?}] seq {name} plain {r:?}").unwrap();
+ }
+ for (w, d) in &plain.steps {
+ writeln!(local, "[{mode:?}] seq {name} step {w}: {d}").unwrap();
+ }
+ for (w, l) in &plain.reached {
+ writeln!(local, "[{mode:?}] seq {name} reached {l} by {w}").unwrap();
+ }
+ for n in &plain.notes {
+ writeln!(local, "[{mode:?}] seq {name} note {n}").unwrap();
+ }
+ }
+ }
+ writeln!(sum, "sequences whose steps, Loads, blocks or accesses differ with _INIs first: {differs}; K1-class writes over all sequences, blocks' loads included: {all_k1}").unwrap();
+
+ // --- K2 through memory: bytes an _OSC sequence reads that an _INI writes.
+ writeln!(sum, "\n## M1b: what the sequences read that the _INIs write (K2 through memory)").unwrap();
+ let mut written: BTreeMap<u64, (String, u8, u8, u8)> = BTreeMap::new(); // byte -> (ini, width, value byte, dump byte)
+ for mode in [Mode::Deny, Mode::Zero] {
+ let mut hh = b.host(mode);
+ let (mut ii, _) = b.load(&mut hh);
+ for p in inis {
+ let before = hh.recs.len();
+ let r = ii.evaluate(&mut hh, p, &[]);
+ let mut w: BTreeMap<String, usize> = BTreeMap::new();
+ for x in hh.recs[before..].iter().filter(|x| x.write) {
+ *w.entry(format!("{} {}B {}", x.space, x.width, x.class)).or_insert(0) += 1;
+ }
+ if !w.is_empty() {
+ writeln!(sum, "[{mode:?}] {p} writes {w:?} and ends {}", match &r { Ok(_) => "having run".to_string(), Err(e) => kind(e) }).unwrap();
+ }
+ for r in hh.recs[before..].iter().filter(|r| r.write) {
+ if let Some(rg) = mem_bytes(r) {
+ for (k, a) in rg.enumerate() {
+ let dump = b.nvs.iter().find_map(|d| a.checked_sub(d.base).and_then(|o| d.bytes.get(o as usize).copied())).unwrap_or(0);
+ written.insert(a, (p.clone(), r.width, (r.value >> (8 * k)) as u8, dump));
+ }
+ }
+ }
+ }
+ }
+ writeln!(sum, "bytes the _INIs write in SystemMemory, both modes: {}", written.len()).unwrap();
+ let mut groups: BTreeMap<(String, u8), (Vec<u64>, BTreeSet<String>, bool)> = BTreeMap::new();
+ for (a, sets_reading) in &read_bytes {
+ if let Some((ini, w, v, d)) = written.get(a) {
+ let g = groups.entry((ini.clone(), *w)).or_insert((Vec::new(), BTreeSet::new(), true));
+ g.0.push(*a);
+ g.1.extend(sets_reading.iter().cloned());
+ g.2 &= v == d;
+ }
+ }
+ writeln!(sum, "of them read by a sequence without _INIs: {}, in {} field(s)", groups.values().map(|g| g.0.len()).sum::<usize>(), groups.len()).unwrap();
+ // Which field each is, by reading every field unit once and seeing what it touches.
+ let fields: Vec<String> = walk(&i).into_iter().filter(|(_, _, k)| *k == Kind::FieldUnit).map(|(p, _, _)| p).collect();
+ let mut field_of: BTreeMap<u64, Vec<String>> = BTreeMap::new();
+ if !groups.is_empty() {
+ let want: BTreeSet<u64> = groups.values().flat_map(|g| g.0.iter().copied()).collect();
+ for f in &fields {
+ let mut fh = b.host(Mode::Deny);
+ let _ = i.evaluate(&mut fh, f, &[]);
+ for r in &fh.recs {
+ if let Some(rg) = mem_bytes(r) {
+ for a in rg {
+ if want.contains(&a) {
+ field_of.entry(a).or_default().push(f.clone());
+ }
+ }
+ }
+ }
+ }
+ }
+ for ((ini, w), (bytes, readers, equal)) in &groups {
+ let mut names: BTreeSet<String> = BTreeSet::new();
+ for a in bytes {
+ for f in field_of.get(a).into_iter().flatten() {
+ names.insert(f.clone());
+ }
+ }
+ writeln!(
+ sum,
+ "{ini} writes {w}-byte accesses there; {} byte(s) of it read; the field(s) reading them: {names:?}; the _INI's bytes equal the dump's (Linux's _INIs ran before it): {equal}; read by {} of the {} sequence cap sets",
+ bytes.len(),
+ readers.len(),
+ sets.len()
+ )
+ .unwrap();
+ writeln!(local, "ini-written {ini} w{w} bytes {:x?}", bytes).unwrap();
+ // Forced to 0x00 and to 0xFF, without _INIs: does any sequence come out otherwise?
+ for v in [0x00u8, 0xFF] {
+ let force: Vec<(u64, u8)> = bytes.iter().map(|a| (*a, v)).collect();
+ let mut changed = Vec::new();
+ for (name, pc, cc) in &sets {
+ let base = sequence(&plan, Mode::Deny, &[], *pc, *cc, &[]);
+ let f = sequence(&plan, Mode::Deny, &[], *pc, *cc, &force);
+ if base.steps != f.steps || base.reached != f.reached || base.blocks != f.blocks || k1_writes(&base.recs).len() != k1_writes(&f.recs).len() {
+ let fr: Vec<String> = f.reached.iter().map(|(_, l)| l.clone()).collect();
+ let br: Vec<String> = base.reached.iter().map(|(_, l)| l.clone()).collect();
+ changed.push(format!("{name} (Loads reached {br:?} -> {fr:?}; steps equal: {})", base.steps == f.steps));
+ }
+ }
+ writeln!(sum, " forced to {v:#04x} [Deny]: sequences whose results, Loads or K1-class writes change: {} {changed:?}", changed.len()).unwrap();
+ }
+ }
+} |
|
M1 on complete tables, part 7 of 7: the AMD bench's SSDT8 without its What the block holds, from
The result. Cut before its The run's log, whole:
# Top-level terms of one definition block (ACPI 6.5 §20.2): opcode, name, length.
import sys
b = open(sys.argv[1], 'rb').read()
def pkglen(i):
lead = b[i]; n = lead >> 6
if n == 0: return lead & 0x3F, i + 1
v = lead & 0x0F
for k in range(n): v |= b[i + 1 + k] << (4 + 8 * k)
return v, i + 1 + n
def name(i):
s = ''
while b[i] in (0x5C, 0x5E): s += chr(b[i]); i += 1
if b[i] == 0x00: return s, i + 1
if b[i] == 0x2E: return s + b[i+1:i+5].decode() + '.' + b[i+5:i+9].decode(), i + 9
if b[i] == 0x2F:
n = b[i+1]; segs = [b[i+2+4*k:i+6+4*k].decode() for k in range(n)]; return s + '.'.join(segs), i + 2 + 4 * n
return s + b[i:i+4].decode(), i + 4
i = 36
ops = {0x10: 'Scope', 0x14: 'Method', 0x08: 'Name', 0x15: 'External', (0x5B, 0x82): 'Device', (0x5B, 0x80): 'OperationRegion', (0x5B, 0x81): 'Field', (0x5B, 0x83): 'Processor', (0x5B, 0x85): 'ThermalZone', (0x5B, 0x84): 'PowerResource', 0xA0: 'If'}
while i < len(b):
op = b[i]
if op == 0x5B:
k = (0x5B, b[i+1]); start = i + 2
else:
k = op; start = i + 1
kind = ops.get(k)
if kind is None:
print(f'+{i}: opcode {b[i]:#04x} {b[i+1]:#04x} not decoded here; stop'); break
if kind in ('Name', 'External'):
n, j = name(start)
print(f'{kind} {n}')
if kind == 'External':
i = j + 2; continue
# skip a Name's data object only if simple; stop otherwise
print(' (stopping at a Name: its data object is not walked here)'); break
ln, j = pkglen(start)
end = start + ln
if kind in ('OperationRegion',):
n, _ = name(start); print(f'{kind} {n}'); break
n, _ = name(j) if kind != 'If' else ('', j)
print(f'{kind} {n} ({ln} bytes)')
i = end
# For each occurrence of a NameSeg in a block, the innermost Method and Device
# whose PkgLength covers it (a backward scan for 0x14 / 0x5B 0x82 whose decoded
# range covers the offset and whose name decodes; a heuristic, not a parse).
import sys
b = open(sys.argv[1], 'rb').read()
def seg_ok(x):
return len(x) == 4 and (chr(x[0]).isupper() or x[0] == 0x5F) and all(chr(c).isupper() or chr(c).isdigit() or c == 0x5F for c in x[1:])
def pkg(i):
lead = b[i]; n = lead >> 6
if n == 0: return lead & 0x3F, i + 1
v = lead & 0x0F
for k in range(n): v |= b[i + 1 + k] << (4 + 8 * k)
return v, i + 1 + n
def name(i):
s = ''
while i < len(b) and b[i] in (0x5C, 0x5E): s += chr(b[i]); i += 1
if b[i] == 0x2E and seg_ok(b[i+1:i+5]) and seg_ok(b[i+5:i+9]): return s + b[i+1:i+5].decode() + '.' + b[i+5:i+9].decode()
if b[i] == 0x2F:
n = b[i+1]; segs = [b[i+2+4*k:i+6+4*k] for k in range(n)]
if all(seg_ok(x) for x in segs): return s + '.'.join(x.decode() for x in segs)
return None
return s + b[i:i+4].decode() if seg_ok(b[i:i+4]) else None
def enclosing(off, opc):
best = None
for p in range(36, off):
if b[p:p+len(opc)] != opc: continue
q = p + len(opc)
try:
ln, j = pkg(q); n = name(j)
except IndexError:
continue
if n and q + ln > off and q + ln <= len(b):
best = n # later p wins: the innermost
return best
for want in sys.argv[2:]:
i = 0
while True:
i = b.find(want.encode(), i)
if i < 0: break
print(f'{want}: in Method {enclosing(i, bytes([0x14]))}, in Device {enclosing(i, bytes([0x5B, 0x82]))}')
i += 1
# Cut one definition block before the term at byte offset `cut`, which must
# be a Scope (0x10) with a two-byte PkgLength and a root-relative name; set the
# header's length and make the checksum (ACPI 6.5 §5.2.6) whole again.
import sys
p, cut = sys.argv[1], int(sys.argv[2])
b = bytearray(open(p, 'rb').read())
assert b[cut] == 0x10 and b[cut + 1] >> 6 == 1 and b[cut + 3] == 0x5C, bytes(b[cut:cut + 8])
b = b[:cut]
b[4:8] = len(b).to_bytes(4, 'little')
b[9] = 0
b[9] = (-sum(b)) & 0xFF
assert sum(b) & 0xFF == 0
open(p, 'wb').write(b)
print(f'cut to {len(b)} bytes; checksum sums to zero')The run's |
|
M1's result on complete tables: K1–K5 decided. This supersedes what the first result left undecided for lack of the SSDTs. The logs are posted whole:
They hold counts and decoded values only. The inputs
How it ran
K1–K5K1 (
K2 (
What the walk writes, now on S1's path. These are K1-class writes, on the T14:
So S1 waits on S2's ruling on AML writing K3 (a
K4 (a bench's processors define
"Never evaluate K5 (the AMD's processors have no
For the staging, beyond K1–K5S1 loads a block on the T14. With DWORD 2 = 0 for both UUIDs, the first processor's commit reaches the Which capability bit loads which block on the T14, each bit passed alone:
For #870: bits 8 and 9 alone load no Linux's capabilities, by halves.
The processors' query answers.
The T14's The AMD's SSDT8 is refused whole, and Linux keeps most of it (part 7).
Question for the orchestrator, and through it the owner: "finished" line 4 ("every block the firmware loads loads, and no method the server evaluates is refused") and S2's exit ("all 17 tables loaded") cannot hold on the AMD while a block that breaks a rule is refused whole.
My recommendation is (a). It is the refusal by name the trust boundary already makes, and (b) is a tolerance written for one firmware's defect. Appendix: the order of accesses in two sequencesRead from the extended harness's
# One M1b sequence's accesses from local.txt, in order: index, direction,
# width and the map's type of each write and each 2-byte read. No address.
import re, sys
path, seq = sys.argv[1], sys.argv[2]
recs = [l for l in open(path) if l.startswith(f'[Deny] seq {seq} plain ')]
print(f'{seq}: {len(recs)} accesses')
for k, l in enumerate(recs):
write = 'write: true' in l
width = re.search(r'width: (\d+)', l).group(1)
cls = re.search(r'class: "([^"]*)"', l).group(1)
if write or width == '2':
print(f' #{k}: {"write" if write else "read"} {width}B {cls}')Its log, whole: |
The owner accepted #875's design with "do the full aml in one pr", so the six slices are no longer six pull requests. M1, run on both benches' complete tables, fixes their order inside it. The T14's processors' _OSC read \OSYS, which \_SB.PC00._INI writes; forced to zero, it loses a block under CPPC. So the _STA/_INI walk runs before _OSC. That walk writes SMI_CMD and memory outside the map, so S2's ruling on those writes comes first. What M1 found false in the track: - no processor _OSC records in memory what it was passed, so the capabilities differential is by set (every bit loads the eight blocks Linux lists), not by Linux's recorded value; - the DSDT's one LoadTable byte pair is data inside a Buffer, not an opcode, so the Load bullet is S1's alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
|
Owner's rulings on M1's two questions, recorded by the orchestrator. These follow the M1 result in #875 (comment).
The design stands as accepted, built as this one PR covering S1–S6, in the order M1's K1–K5 set. |
The owner's "finish aml then continue working on deep idle states" (#870, comment 6119715385) puts the interpreter ahead of deep idle states. The track now says what finished is, held to ACPI 6.5 and to Intel's 302223-007 for a processor's _OSC, and the six slices that reach it in order, each with its exit: Load and _OSC first, which deep idle waits on. The design, its citations and its roast are #875's body. The track is assigned, S1 held by #875, and its Load bullet now names its owner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
The owner accepted #875's design with "do the full aml in one pr", so the six slices are no longer six pull requests. M1, run on both benches' complete tables, fixes their order inside it. The T14's processors' _OSC read \OSYS, which \_SB.PC00._INI writes; forced to zero, it loses a block under CPPC. So the _STA/_INI walk runs before _OSC. That walk writes SMI_CMD and memory outside the map, so S2's ruling on those writes comes first. What M1 found false in the track: - no processor _OSC records in memory what it was passed, so the capabilities differential is by set, not by a value an OS recorded; - the DSDT's one LoadTable byte pair is data inside a Buffer, not an opcode, so the Load bullet is S1's alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
… firmware memory through the kernel The owner's rulings on PR #875 (comments 6120173372 and 6121646183) settle what a machine's AML may reach. Reads. `toyos_userbound::firmware::decide` passes a read of memory the firmware's map types EfiMemoryMappedIO (UEFI type 11) on exactly the terms an unlisted register's read already passes: at or above 1 MiB, inside the direct map, typed uncacheable by the boot processor's range registers, on a machine none of whose CPUs holds registers that are on and differ, and outside every page a device the kernel knows decodes in (a driven window, any function's memory BAR, the local APIC). A write there stays refused MemoryType. The refusal of a register read the range registers would cache is renamed RegisterCached, since it now names a typed register too. The bound stays address-only: the regions a machine's tables define are known only to the holder's interpreter, which the kernel does not run. Writes. The server asks the kernel for a byte to the FADT's SMI_CMD, read through the same `toyos_acpi::fixed_hardware` decode the kernel declares the port by, which the kernel makes as a firmware call or refuses by byte and rate. Every SystemMemory write AML makes goes to the kernel, which passes NVS and reserved memory and refuses the rest by name: a write outside every range the firmware's map lists comes back MemoryType in unlisted memory, is denied and counted. Configuration writes stay denied. Checks: a host test of each arm of the MMIO read (each width, the last byte, cached, below 1 MiB, CPUs differing, unmapped, a device's page, the local APIC, an ECAM window typed MMIO, a read into RAM beside it); the server's port test gains SMI_CMD at a byte and refused at a word, and a memory-write test holds the kernel's made and refused writes and the ledger's count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…ontroller under its _GLK What OSPM runs of a machine's AML once its tables load (ACPI 6.5 §6.5.1, §6.5.4), built before S1 because the T14's processors' _OSC read \OSYS, which \_SB.PC00._INI writes (#875's M1, K2). In order (`init.rs`): - `_REG(2, 1)` in each scope that declares a PCI_Config region not on a root bus naming a _BBN, which §6.5.4 alone makes always available: the kernel reads configuration space from the start, so these are announced before any _INI reaches them; - \_SB._INI, unconditionally; - every device's _STA and _INI by Table 6.67, parent before child, a Processor object a device; each _STA said under OWN; - again over whatever an _INI loaded, each new _INI by the same table. The embedded controller. The owner ruled "follow the spec" on the ECDT (#875, comment 6120208838): the controller the ECDT names is attached to AML before the first table loads (`toyos_acpi::ecdt`, which now reads EC_ID's path too), and the namespace's PNP0C09 device is served in its place once found. Its `_GLK` is honoured: 1 takes the Global Lock around each transaction, AML's and the server's own queries alike, unless AML already holds it; another value leaves the space unattached, said. Its `_REG(3, 1)` runs in each scope that declares an EmbeddedControl region, after every _INI, as the track's _REG bullet asks. The interpreter finds a PCI root bridge that names no _BBN by its _HID or a _CID, PNP0A03 or PNP0A08 (§6.1.5, §6.1.2), on bus 0; a region below no host bridge is a broken rule, refused by name. Its walk entries carry each object's place in definition order, so the server knows what a load added (`Entry::serial`, `Interpreter::defined`), and a region's space. Two issues close by their exits: QEMU 11.1.1's \_SB.LNKA._CRS evaluates to a buffer on the fixture, and a controller whose _GLK is 1 is driven by a stand-in that refuses a transaction made without the lock. A reading for the orchestrator: on QEMU, \_SB.CPUS.Cxxx._STA writes QEMU's CPU hotplug selector (SystemIO 0xcd8, a dword), which the server writes for no AML, and \_SB.HPET._STA reads the HPET the kernel drives; both are refused by name and their devices taken as absent, pinned in `qemus_tables_load_and_s5_is_what_its_kernel_decoded`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…gotiated Load (ACPI 6.5 §19.6.76). `Load (Object, Result) => Boolean`, encoded `LoadOp NameString Target`, from a SystemMemory region, a field unit of one at whole bytes, or a Buffer, at the root, in the DSDT's integer width. The block's header, length and checksum are checked before any of it runs, and it must lie wholly in its source. Its bytes come through a new host call, which the server answers from the kernel's reads only where every read's memory type is AddressRangeReserved or AddressRangeNVS by Table 15.6 (UEFI types 0, 5, 6, 8, 11, 12, 13 and 15 up, and 10; ACPI reclaim, 9, is neither). A refused block (malformed, colliding, failing its header, length or checksum, or denied by the host) loads false and keeps nothing; a bound of the evaluation, or Fatal, refuses the evaluation, whose steps, heap and time the block shares. Each run-time load is told to the host, which says its signature and length, and under OWN its OEM Table ID and the method that loaded it. Two arenas. §5.5.2: "Global objects are those NameSpace objects created at table load time", and a method's own objects die at its exit (§5.5.2.3). A Load inside a method interleaves the block's nodes with the method's, so one stack would unwind both: the namespace now keeps a static arena, for what definition blocks define, and a method arena, each a stack. A NodeId names its arena (the top bit of a four-byte link, so a child map's entry is no wider), a frame defines into its own, and a static object below a method's, or a static Alias of one, is refused. The walk reads the static arena; the server says its slots after the machine's last table, the track's arena bullet read rather than derived. The cost is the design's: a method that defines names grows the method arena's own slots where the single arena often had room. One heap test filled within 1.3 MB of the 16 MiB bound with a method naming 20,000 objects; measured at 13.03 MB held, it now crosses the bound, so it names 15,000 below 30,000 devices for the same claim. The arena-move charge case fills the method arena with the method's own names, since a definition in a method no longer moves the tables' arena. _OSC (§6.2.11; Intel 302223-007 §2.2). After the controller is armed, \_SB._OSC under the platform-wide UUID, then, where the processor is Intel's (CPUID leaf 0, `toyos_abi::processor_is_intel`), each Processor object's and ACPI0007 device's _OSC under Intel's, each a query and then a commit of what the query offered of what was asked. The claim is one declaration a UUID, each zero with each bit's reason (`osc.rs`). The answer is untrusted: its length must be what was passed, its status read by name, an unasked bit dropped; a query answering failure or an unrecognised UUID is not committed, and Capabilities Masked does not repeat the query, since the T14 answers it to every set but one (M1). What a commit loads is initialised by S3's walk in turn. Checks: host tests of Load from each object, from a method and at definition-block level, the method's own objects gone and the block's kept, each refusal false and keeping nothing, a block below a method's object or aliasing one refused, a bound refusing the evaluation, the DSDT's width; and a server test in which the firmware offers every bit and the commit sends none, the processor's commit loads a block from reserved memory whose device's _INI then runs, a non-Intel processor's _OSC is not evaluated, and a block in reclaim memory is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
The controller's attachment is the devices module's, beside the search that finds the controller and the query that runs its methods, so the check outside the tree drives the server's own modules without main.rs's claim and ports. No behaviour changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…ableRegion, and Error::Unsupported deleted Every address space of Table 5.182 now reaches the host (ACPI 6.5 §5.5.2.4), which says which it serves: - by offset, an access unit at a time, as an `Address`: SystemCMOS a byte of the RTC/CMOS device's memory (§5.5.2.4.3); PciBarTarget by the BAR at the region's offset in the function's configuration space, its length the BAR's (§5.5.2.4.4); PCC by subspace and offset into its shared memory (§5.5.2.4.8); and each OEM-defined space; - whole, as a `Transfer`, where the protocol is command- or connection-based: SMBus, IPMI, GeneralPurposeIO, GenericSerialBus, PlatformRtMechanism and FFixedHW. A field keeps the protocol its AccessAs names (Table 5.184; an AccessType byte's bits 7:6 name AttribBytes, AttribRawBytes or AttribRawProcessBytes of a count, an ExtendedAccessField both) and the descriptor its Connection names, inline or by name, with the bit its units begin at. A BufferAcc field's value is the buffer the host answers, and a Store to it answers the response its buffer became, as `BUFF = (FLD = BUFF)` reads it (§5.5.2.4.5.1, §5.5.2.4.7.1); a GeneralPurposeIO field's is its pins. A reserved space, BufferAcc in a space addressed by offset, and a BankField of a carried space break a rule, refused by name. DataTableRegion (§19.6.25) and LoadTable (§19.6.77) find the table the XSDT lists by signature, OEM ID and OEM Table ID through a new host call (`toyos_acpi::find_by_id`, a shorter identifier matching a field padded with NULs or spaces), which the server answers only where the table lies in memory Table 15.6 calls reserved or NVS: QEMU's and most machines' tables are in ACPI reclaim memory, and denied. LoadTable loads below its RootPathString, stores ParameterData once loaded, and is false for no table, a denied one, or a refused block; an identifier longer than its field is a run-time error. Neither bench's tables use either (#875's M1). A reference to an unnamed object handed to the caller is `Value::Unnamed`, what it referred to; a LocalX's is gone with its method. `0x5B 0x2A`, Unload until ACPI 6.5 reserved it, stays malformed. With that nothing of the grammar is refused as not carried, and Error::Unsupported is deleted. The server serves SystemMemory, SystemIO, PCI_Config and EmbeddedControl, and denies every other space by name. Checks: a host test for each construct (tests/spaces.rs, tests/load.rs, toyos-acpi's corpus); the server's denial of each space and its lookup's memory types. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…he SCI through a load's Sleep
GPEs (ACPI 6.5 §5.6.4). Each `\_GPE._Lxx` or `_Exx` in GPE0 is a runtime
GPE and enabled, but one the controller's (its driver serves it,
§5.6.4.1.2), one naming both triggers, and one the `_PRW` of a device
other than the power, lid and sleep buttons and the PCI host bridge names:
§5.6.4.2 keeps wake events off runtime inputs, and OSPM enables a wake
event only to arm a device's wake, which ToyOS does for none
(`gpe.rs`). On its SCI a GPE's enable is cleared, an edge's status
cleared, its method run, a level's status cleared, and its enable set
again; a method refused leaves it disabled, said by name and served no
more. The census says how many GPEs have a method, how many are runtime,
and how many wake events stay disabled.
Notifies (§5.6.6). The host keeps each Notify but a control-method
button's press, once an object and a value, and the server delivers them
after each evaluation: a battery's or adapter's 0x80 reads the power
sources again, a battery's 0x81 finds them again, `\_SB`'s or a negotiated
processor's 0x08 negotiates its `_OSC` again with the same claim
(§6.2.11), a processor's 0x80 to 0x82 is counted since nothing reads its
performance, idle or throttling states yet, and anything else is served by
nothing; each is said once.
The SCI while a table loads. An evaluation's Sleep waits on the SCI to its
deadline (`sci::await_sleep`): a press of the fixed button and the
controller's GPE are taken as they latch and handed to the server, which
serves them after the evaluation, the press powering the machine off once
the load has handed `\_S5` over; anything else keeps its status and loses
its enable until the Sleep ends. That meets the exit of the track's bullet
on a press during the load, which waited out the load's sleeps.
`acpi_press_across_a_lock_wait`'s table waited for the press in its load
by `While (!PBST) { Sleep (1) }`, reading PM1 status directly, which
§5.5.2.4.1 forbids AML; a Sleep now takes that press, so the fixture waits
by Stall, which keeps the processor and serves nothing. What the test
holds, a press latched across the Global Lock's wait, is unchanged.
Checks: host tests of the GPE census and handling, a refused method's GPE
disabled, each Notify's delivery, what a Sleep took served after it, and
the Sleep's wait taking a press and the controller's GPE and leaving the
rest latched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
… the longest wait, the firmware's longest hold The heap. The interpreter states what one load or evaluation holds while it runs, `MAX_RUNNING`: `MAX_LIVE` and what a run holds beyond the meter's count, each part bounded by another bound of the interpreter and named at the constant (an arena's old slots while it doubles, one operator's bytes before the meter holds them, a table's while it is read in, each frame's slots, name and PCI walk, each Mutex held, a node's path). Mutexes held were bounded only by the steps, about 4.5 MiB of records: an evaluation now holds at most 256 acquisitions at once, refused at the bound as nesting is. Every hostile fill of `tests/heap.rs`, by loads and by methods, holds its peak under the counting allocator to `MAX_RUNNING`. The server says both bounds beside what the interpreter holds after the machine's last table, and the static arena's slots. The ten-second wait. The census says the longest wait an evaluation of the initialisation asked, refused or not (`Usage::waited_us`), the reading the track's bullet asks of the T14. The Global Lock. The host measures how long a take waited for the firmware's release, the firmware's longest hold as a take saw it, and the load's line says it beside the contended count, the reading the track's bullet sets the bound by. What a full interpreter does is a design the accepted one does not hold, and is proposed on the pull request rather than built here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…eads S1, S3 and S5 on the machine `acpi_tables_loaded` now judges the bench whose ACPI row the kernel names, by that bench's expectations (`Bench` in tests/toyos.rs): the T14's 14 blocks, the AMD bench's 17 with SSDT8 refused for the name its `Scope` does not resolve, as the owner ruled, and each `\_S5`. No row names its hardware yet, so one row that reads either bench as itself costs no red on the other. On either bench it reads what #875 built: the namespace initialised with every `_INI` run and none refused but by the owner's ruling on a write outside every range the map lists, and `_REG(3, 1)` after them; `\_SB._OSC` negotiated, and on the T14 each processor's, none refused; the blocks ToyOS's claims load, by signature and length, the T14's one of 459 bytes as the check outside the tree read it; and it prints the census, the GPEs enabled, the longest wait asked, the firmware's longest hold of the Global Lock, the interpreter's heap and the namespace's slots. `acpi_server_events`, on the held boot, reds on a runtime GPE whose method was refused and prints each GPE served. The issues: the track says what the build met and what each stage still waits on, its `_REG` bullet and the press's wait gone with their exits met; the AMD bench's SSDT8 is recorded as the firmware defect the owner ruled it, with its evidence and an exit; and the AMD bench's memory-mapped I/O issue records the rulings that bring its exit to 16 of 17 tables. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…ed does The T14's round-4 boot at ddc243e read battery_read red on "a SystemMemory write the kernel refused MemoryType, in unlisted firmware memory": the refusal the owner ruled on #875 (a memory write outside every range the map describes is refused by name and recorded, not admitted), the one the T14 bench's `ruled` names, and the one acpi_tables_loaded held met on the same boot. battery_on_metal kept its own refusal filter and never consulted the bench's; it now does, so any other refusal still reds it. The write, from the T14's own tables run on the host and read by hand: the embedded controller's _INI calls its hotkey device's wireless set-up, which on a flag the firmware's SMI answers drives one GPIO pad's TX state to 1 through the tables' own PCR helper. It is one byte, 0x01, at SBREG_BAR + Port ID 0x6E + 0xA70: PAD_CFG_DW0 of GPP_A13 in GPIO Community 0, bit 0 GPIOTXSTATE (Intel 500 Series Chipset On-Package PCH datasheet vol. 2, 631120-002, section 27.1.114), inside a 64 KiB window the GPIO controller (INT34C5) lists in its _CRS, and in no range of the firmware's memory map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…utside the map The cost of the owner's ruling on the T14, recorded as the AMD bench's is: the controller's _INI stops at a GPIO pad's TX state in the PCH's GPIO Community 0 (a window the GPIO controller's _CRS lists and the firmware's map does not), read on the T14 at ddc243e and reproduced on the host by the T14's own tables; the hotkey device's _INI stops at a write the T14's log does not detail, which the host run puts in the processor's MCHBAR. The T14 bench's doc points at it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…ed does The T14's round-4 boot at 639ce13 read battery_read red on "a SystemMemory write the kernel refused MemoryType, in unlisted firmware memory": the refusal the owner ruled on #875 (a memory write outside every range the map describes is refused by name and recorded, not admitted), the one the T14 bench's `ruled` names, and the one acpi_tables_loaded held met on the same boot. battery_on_metal kept its own refusal filter and never consulted the bench's; it now does, so any other refusal still reds it. The write, from the T14's own tables run on the host and read by hand: the embedded controller's _INI calls its hotkey device's wireless set-up, which on a flag the firmware's SMI answers drives one GPIO pad's TX state to 1 through the tables' own PCR helper. It is one byte, 0x01, at SBREG_BAR + Port ID 0x6E + 0xA70: PAD_CFG_DW0 of GPP_A13 in GPIO Community 0, bit 0 GPIOTXSTATE (Intel 500 Series Chipset On-Package PCH datasheet vol. 2, 631120-002, section 27.1.114), inside a 64 KiB window the GPIO controller (INT34C5) lists in its _CRS, and in no range of the firmware's memory map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
|
Ruling on the GPIO pad write ( |
|
Metal, run by the orchestrator. Round 4 at |
|
Review, round 5, at None of the PR's 20 comments is an earlier review, so each earlier ruling is judged against this head instead:
BLOCKER
NOTE
Growth (
Round 5 changes no production code: against SEND BACK |
|
Ruling on the commit messages (round 5 NOTE): commit messages are |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
Round 5's review of #875: the T14 judges excused every line that carried the ruled kind, so acpi_tables_loaded passed a third MemoryType refusal of any path and a census of x2 or x9 alike, and battery_read passed a _BST refused MemoryType. A bench's ruled refusal is now a Ruled: its kind, each line the server says of it by kind, each evaluation it refuses by the path the server says under OWN, and the census's count of it, exactly. Every line carrying the kind but the census is one of those, and each of those is there. battery_read holds the T14's the same way. The judge prints the ruled lines that say a kind, and none of the server's OWN lines. OWN moves to acpiserver-api, which the judge now reads it through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…in Server::loaded, and acpi_press_across_a_lock_wait is cut Round 5's review of #875: acpi_press_across_a_lock_wait booted QEMU, launched acpiserver, pressed the button and judged the order of the server's main, an integration test of a userspace program, which reviewer.md's Tests refuses (#890). A module test holds the claim instead. main's order, the load's sleep type and namespace taken, what a Sleep latched during the load served, the SCIs already latched served, then the namespace initialised, moves into Server::loaded, generic over the Kernel. For that, the claim's SCI record, its acknowledgement and the power-off asked of the supervisor are Kernel's: Claim answers them as main did, serve reads the SCI through the same latched, and Scripted answers no SCI and panics with POWERED_OFF where it is asked to power off, as a stop that happens takes the process with it. a_press_latched_during_the_load_powers_off_before_an_ini_that_waits_on_the_firmware: a DSDT whose Sleep takes a press as it loads, and whose \_SB.GLI._INI Acquires \_GL with no bound while the firmware holds it (takes: [Pending], no release), is loaded and handed to Server::loaded; the machine powers off and the take is never asked. Initialising before the latched press reds it: the _INI's take finds the firmware holding the lock past RELEASE. The cut takes what only that test used: acpi_mediated's held arm, BootOptions::acpi_tables and its -acpitable plumbing, and the count acpi_tables_loaded answered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…the GPIO and MCHBAR ruling; the AMD bench's MMIO read is closed Round 5's review of #875. The track's finish and S3's exit asked every block loaded and no method refused on both benches, and readings equal to Linux's: the first is false under the owner's rulings (SSDT8, the AMD bench's ConfigWrite, the T14's two _INIs), and a reading under another OS is evidence, never an exit's oracle (#902). Both now state the ruled state, ACPI 6.5 deciding. The T14 issue records the owner's ruling on #875 (comment 6135767889): the GPIO pad write and the MCHBAR write stay refused, isolation deciding, and the row expects both _INIs by path. acpiserver-refuses-the-amd-benchs-dsdt-for-a-read-in-memory-mapped-io.md is closed: its exit is met. The policy was ruled (comment 6120173372), firmware::decide holds it with a host test of each arm, and the AMD bench's boot at ddc243e (639ce13 before round 6's reword, the same tree) read 16 of 17 tables loaded, SSDT8 the one refused as ruled, and \_S5 handed, SLP_TYPa=5: acpi_tables_loaded passed, judge exit 0 (https://github.com/ToyOSOrg/evidence/blob/063f2e24002d1f506a59c4440f6da9b300753f7a/round6/metal/amd-judge.log, and judged again at 5352b09 in round6/judge/fixed-amd.log, exit 0). Two issues name the branch's commits by their hashes after the reword. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
|
T14 at |
|
Review, round 6, at The reword. I paired the 32 commits of Round 5's BLOCKERs
Round 5's NOTEs: Must the AMD bench boot at this head? No.
BLOCKER
NOTE
Growth (
Round 6 alone ( SEND BACK |
… names what reads _CST The finish asked every battery reading with no value unknown, which the AMD bench cannot meet under the ruling on its _PSR: its adapter reads `AC unknown`. It now states that exception. `_CST` left the finish in the previous commit without a word on where it went: no part of this track reads it, and the deep idle states track does (#870). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
|
Review, round 7, at Round 6's BLOCKER
Round 6's NOTEs
Since
No new finding. Growth ( LAND |
#893 cut acpi_power_button and acpi_press_across_a_lock_wait and replaced the latter's held arm with acpi_press_is_one_sci's press arm; this branch had cut acpi_press_across_a_lock_wait too. The conflicts take main's side in tests/common/qemu.rs (both sides dropped acpi_tables; main also drops second_slot), tests/guest/rust-tests/src/bin/acpi_mediated.rs (the press arm; this branch's RegisterCached rename auto-merges) and tests/toyos.rs's RUST_SKIP comment. In tests/common/power.rs acpi_power_button goes with main, which leaves power::acpi_tables_loaded with no caller, since this branch's acpi_tables_on_metal reads each bench's blocks itself: it goes, with ACPI_TABLE, ACPI_S5_HANDED and slp_typ_a. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
|
T14 at |
|
Review of the merge Conflict accounting. Six files changed on both sides:
The four deletions are dead.
Evidence at
Growth against main after the merge ( BLOCKER
NOTE
SEND BACK |
|
Re-review of the merge Earlier BLOCKER
The earlier NOTE (the BLOCKERNone. NOTE
LAND |
This takes up
issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md. The design and its roast were accepted as one pull request of six stages (comment 6120208838, the owner's "do the full aml in one pr"), in the order M1 set (its result). The owner's rulings it builds on:_INIwalk may write, and the AMD bench's SSDT8, comment 6121646183;_REG, comment 6126449763, with the orchestrator's reading of them in 6130332818;Every citation is to ACPI 6.5 (the UEFI Forum's release source,
UEFI/ACPI-Specification-Releaseat5dabe6f37f), to Intel's 302223-007 for a processor's_OSCand 332691-003 for the 8259's edge/level control, and to AMD's PPR 55922-A1 rev 3.06; where the specification is silent the site states ToyOS's own rule. No other implementation is cited, in the tree or in a commit message: round 4 re-cited every site that cited one, and round 6 reworded the seven commit messages that did. The reword changed no tree, so every commit before round 6 has a new hash: each is mapped to the one it became, trees equal, inround6/reword-map.txt. Round 5's heada99fa7c32isbf6a51068, and round 4's639ce1374isddc243eaf. This body names the new hashes. The evidence repository's earlier commits and files keep the old ones.Each stage is its own commit, in the order built: S2, S3, S1, S4, S5, S6, then the rows and the issues. The three rulings of comment 6126449763 are the commits after the merge of
main's36aff86fc, three of code, one of issues and three of tests. Round 4's rulings, 6131125468, are117858e2f,f61d38f94and637409f88. Round 5, on round 4's T14 boot, is01523db5dandbf6a51068, test and issue alone. Round 6, on round 5's review, is5352b0901(the T14 bench by path),1b94d83e1(Server::loadedand the guest test cut) and17c2cb89a(the issues).What changed, by stage
S2: what AML may reach (
d2006cc65,15536e2e0)Memory-mapped I/O.
toyos_userbound::firmware::decidepasses a read of memory the firmware's map typesEfiMemoryMappedIO, on exactly the terms an unlisted register's read already passes:A write there stays refused. The refusal of a register read the range registers would cache is renamed
RegisterCached, since it now names a typed register too.Why this bound, per the read ruling's condition. The ruling asked that the check be the best one, never reach RAM or a claimed device's BARs, and refuse everything else by name.
issues/the-acpi-claims-holder-reaches-every-port-and-firmware-range-the-kernel-did-not-declare.mdis unchanged.Port writes, as the owner ruled (
15536e2e0,117858e2f). Every port write AML makes goes to the kernel, which decides it as it decides a read; the server's own list of three forwarded writes is gone.SMI_CMD, a byte to which it makes a firmware call or refuses by byte and by rate.toyos_userbound::firmware::PROTECTED, refusedProtectedPort: the 8259's edge/level control, ELCR1 and ELCR2 at 0x4D0 and 0x4D1, which Intel lists among the pair's own registers (332691-003 Vol. 2, §28.1, Table 28-1) and which the pair's declaration does not span. The kernel drives the pair, so it is the kernel's whole. The rule is ACPI 6.5's "exclusive access to any address accessed via fields declared in Operation Regions" (§5.5.2.4), which a port of a device the kernel drives cannot give AML._STAwrite the PCI interrupt router's index at 0xC00, are written.Memory writes. Every SystemMemory write goes to the kernel, which passes NVS and reserved memory and refuses the rest by name. A write outside every range the map lists comes back
MemoryTypeand is denied and counted, as the owner ruled. Configuration writes stay denied.S3: the namespace initialised (
50abe34b5,011714fd9)system/acpiserver/src/init.rs):_REG(2, 1)in each scope whose PCI_Config regions are not on a root bus naming a_BBN.\_SB._INI, unconditionally (§6.5.1)._STAand_INIby Table 6.67, parent before child, where an_INIis at or below it._REG(3, 1)in each scope declaring an EmbeddedControl region, after every_INI, as the track's_REGbullet asked._REG(011714fd9, re-cited inf61d38f94)._REGtells AML of the change. So an attached controller is reached whether or not its_REGhas run, and with none attached the space is denied by name, where §6.5.4 has writes ignored and reads indeterminate._INImethods: OSPM "will examine each of the children of the device for _INI methods" and evaluates_STA"before evaluating the _INI object". So a device with no_INIat or below it is not examined. The server's first walk asked every device's_STA, so the AMD bench's battery_STAread the controller's space before the controller was attached, on a machine with no ECDT, and was refused._STAthat is refused or no Integer. ToyOS's rule: it says nothing, so its device's_INIis not run and its children are examined, as Table 6.67's not-present-but-functional row has them.toyos_acpi::ecdt, which now readsEC_ID's path too. The namespace's PNP0C09 is served in its place once it is found._GLK. A_GLKof 1 takes the Global Lock around each transaction, AML's and the server's own queries alike, unless AML already holds it.issues/the-acpi-server-talks-to-the-embedded-controller-without-the-global-lock.mdis closed bya_controller_that_wants_the_global_lock_is_reached_under_it_alone, whose stand-in controller refuses a transaction made without the lock._BBNis found by_HIDor a_CID, PNP0A03 or PNP0A08 (§6.1.5, §6.1.2), on bus 0.issues/qemus-interrupt-links-are-refused-by-the-aml-interpreter.mdis closed byqemus_interrupt_link_reads_its_routing_register_below_a_bridge_named_by_its_hid, which evaluates QEMU 11.1.1's\_SB.LNKA._CRSto a buffer._INI, so the walk examines no device there, pinned inqemus_tables_load_and_s5_is_what_its_kernel_decoded._INI"under a device object", so the walk's examining Device and Processor objects alone is no weakness. The finding round 2 filed against another implementation's walk is deleted inf61d38f94.S1:
Load, two arenas,_OSC(d9ca8cd38,fb2bcceda)Load(§19.6.76) loads from a SystemMemory region, a field unit of one at whole bytes, or a Buffer, at the root.NodeIdnames its arena in the top bit of a four-byte link, so a child map's entry is no wider._OSC.\_SB._OSC, then each Processor object's and ACPI0007 device's on an Intel processor (CPUID leaf 0), each queried and then committed with what the query offered of the claim.osc.rs), both 0, each bit's reason at the constant.S4: the grammar whole (
800fb4770)Address: SystemCMOS, PciBarTarget (the BAR at the region's offset, its length the BAR's), PCC, and the OEM-defined spaces.Transfer: SMBus, IPMI, GeneralPurposeIO, GenericSerialBus, PlatformRtMechanism and FFixedHW. The field carries its AccessAs protocol (Table 5.184) and its Connection.BUFF = (FLD = BUFF)reads it (§5.5.2.4.5.1, §5.5.2.4.7.1).DataTableRegion(§19.6.25) andLoadTable(§19.6.77) find the table by signature, OEM ID and OEM Table ID (toyos_acpi::find_by_id). The server answers only where that table is in reserved or NVS memory, which the specification requires.Value::Unnamed.Error::Unsupportedis deleted.0x5B 0x2Astays malformed.S5: every event served (
b8dbbad45,bac5ccca6,1b94d83e1)gpe.rs, §5.6.4).\_GPE._Lxx/_Exxin GPE0 is enabled, except the controller's, one naming both triggers, and one the_PRWof a device other than the buttons and the PCI host bridge names (§5.6.4.2).\_SB's or a negotiated processor's 0x08 negotiates_OSCagain;sci::await_sleep).\_S5is handed over.\_S5over, before the initialisation, whose evaluations may wait seconds: the presses its Sleeps took, then any SCI already latched (bac5ccca6).Server::loaded(1b94d83e1), generic over theKernel, where it wasmain's. So the claim's SCI record (Kernel::latched), its acknowledgement (Kernel::ack) and the power-off asked of the supervisor (Kernel::power_off) are theKernel's.Claimanswers each asmaindid, andservereads the SCI through the samelatched. The host'sScriptedanswers no SCI, and where it is asked to power off it panics withPOWERED_OFF, since a stop that happens takes the process with it.a_press_latched_during_the_load_powers_off_before_an_ini_that_waits_on_the_firmwareholds it. Its DSDT's top-level Sleep takes a press as the table loads (sleeps), and its\_SB.GLI._INIAcquires\_GLwith no bound while the firmware holds it (takes: [Pending], and no release, so the wait runs to its deadline). The test hands the load toServer::loaded. The machine powers off, and the take is never asked.S6: the bounds stated and read (
cf4ed8a80,eb9419edc)MAX_RUNNINGstates what a load or evaluation holds while it runs:MAX_LIVEand each part held beyond the meter's count, each bounded by another bound and named at the constant.tests/heap.rsholds its peak toMAX_RUNNINGunder the counting allocator.eb9419edc, re-cited inf61d38f94). A method refused at the heap bound ends as any method ends: what it created is "destroyed on exit" (§5.5.2.3), and the Mutexes it holds are relinquished, since "Ownership of a Mutex must be relinquished before completion of any invocation" (§19.6.88) and a refused invocation is one the interpreter completes.finishlets go of every Mutex and the Global Lock. The refusal is the bound's, by name.a_method_refused_at_the_bound_gives_back_what_it_madefills the bound through one method's named object and locals while it holds\_GL, and reads the heap given back to the byte, the next Acquire of\_GLtaking it from the firmware again, and a method of a mebibyte's buffer evaluated after. That is the track's exit for a full interpreter.Usage::waited_us).Rows and issues (
14241dc96,7c46e7dac,637409f88,01523db5d,bf6a51068,5352b0901,17c2cb89a)acpi_tables_loadedjudges each bench by its own expectations (Bench,tests/toyos.rs), told apart by the kernel's line for the FADT's ACPI row. No row names its hardware yet, so one row reading either bench as itself costs neither a red.It is held to the server's
power sources:line, as ruled. The server writes that line, through the newacpiserver_api::POWER_SOURCES, on every machine with a battery once its tables, its initialisation, its_OSCand its first reading are done.acpi_holdwaits for it, and the judge now passes the hold and reads the log only up to that line.The T14: 14 blocks; both
_OSCs with nothing refused; the one block its claims load, 459 bytes; every_INIrun but the two the owner's ruling on a write outside the map refuses;_REG(3, 1)after them.The AMD bench: 16 of 17 tables, SSDT8 refused as ruled,
\_S5handed. It now expects, as it expects SSDT8, the controller's_REG(3, 1)and the adapter's_PSRrefusedConfigWrite, and its census to sayno _REG run.Each bench's ruled refusal is held exactly (
5352b0901), where round 5 excused every line that carried its kind. ARulednames:OWN: the T14's\_SB_.PC00.LPCB.EC__._INIand\_SB_.PC00.LPCB.EC__.HKEY._INI, and the AMD bench's\_SB_.PCI0.LPC0.EC__._REG;x2on the T14 andx1on the AMD bench, compared whole.Every line that carries the kind is one of those, the census aside, and each of those is there. So a third path refused with the same kind, a path changed, or another count reds. The judge prints the lines that say a kind and none of the server's
OWNlines.OWNmoved toacpiserver-api, through which the judge reads it.acpi_server_eventskeeps its claim, a count of the controller's queries, and waits for it in a job of its own,acpi_counted: the old hold's wait, renamed. It runs among the boot's rows' jobs, beforeacpi_hold. A count waits on an event an idle machine may never raise, so it holds no other row's boot: on the AMD bench, whose controller raised none in 52 s,acpi_tables_loadedrides an image without it.acpi_server_eventsalso reds on a runtime GPE whose method was refused, and prints each GPE served.battery_readholds the T14's ruled refusal asacpi_tables_loadeddoes (01523db5d). Round 4's T14 boot read it red on "a SystemMemory write the kernel refused MemoryType, in unlisted firmware memory", the refusal the T14Benchnames andacpi_tables_loadedheld met on the same boot.battery_on_metalkept its own refusal filter and never consulted the bench's. It now excuses only whatT14.ruledsays, by the same two paths, and holds it the same way, so a_BSTrefused with the ruled kind reds it._REGbullet and the press's wait go with their exits met. Its full-interpreter bullet's exit is met as ruled, and its port-write bullet goes with the weakness.17c2cb89a). Every block loads but SSDT8, and no method is refused but the four the rulings name, each expected by its bench by name. Each block a processor's_OSCloads under ToyOS's claims loads. Each_STAthe walk reads is said, and each battery reading is read whole by §6.3.7 and §10.2.2, with no value unknown but the AMD bench's adapter,AC unknownunder its ruled_PSRrefusal (a785962f4)._CST, which the finish named before, is now among what is not in it: no part of the track reads it, and the deep idle states track does (Design: idle CPUs enter the deep idle states the firmware declares #870). They asked readings equal to Linux's before, which a reading under another OS cannot be the exit of (Another implementation's source is read only clean-room, and the tree names Linux only as a host platform or a libc interface #902).issues/the-amd-benchs-ssdt8-scopes-a-controller-no-table-defines.mdrecords SSDT8 as ruled, with its evidence and an exit; it cites §19.6.120, Scope.issues/the-amd-benchs-ec-reg-and-psr-are-refused-for-a-write-to-the-root-complexs-index.mdrecords the ruled config write's cost:_REG(3, 1)refused after it storedOKECandBASC,_PSRrefused so the adapter readsAC unknown, and every later evaluation that reachesALIBrefused.issues/acpiserver-refuses-the-amd-benchs-dsdt-for-a-read-in-memory-mapped-io.mdis closed (17c2cb89a). Its policy was ruled,firmware::decideholds it with a host test of each arm, and round 4's AMD boot read 16 of 17 tables loaded and\_S5handed (Metal).issues/the-t14s-controller-and-hotkey-ini-are-refused-for-writes-outside-the-map.mdrecords the T14's ruled write and its cost, as the AMD bench's issue does (bf6a51068), and now records 6135767889 too: the GPIO community's pad and MCHBAR stay refused, isolation deciding. The T14Benchpoints at it. Its exit: the hotkey device's write read on the T14, and a mediation by Intel's register maps accepted and built, or the refusal stays.Checks
The oracle is the specification at each rule, cited at the code that encodes it. Intel's 332691-003 is the oracle for the ELCR's ports, and AMD's PPR for what the bench's root complex documents. The differential below is a measurement, kept in the evidence, and no row's oracle.
The differential is Linux on each bench, as the design names it: its blocks, its
_STAs and its_CST, from the bench's own dump. The check runs outside the tree: the server's own modules, included by path and driven through theirKernelseam on each dump, in the server's start order. Each port access is now decided as the kernel decides it, bytoyos_userbound::firmware::portover the kernel's declarations rebuilt from the bench's FADT; the TCO block, whose base the dump does not hold, is not among them (round2/check/at da7107e246580b0d6e88748594da6677d32fe175; the T14's summary, the AMD's, its source). It ran at this branch's headb3bfc9742.\_S5is 7._INI, reads 12_STA(56 before) and runs 37_INI. Two_INIs are refused at the ten-second wait, each calling the firmware throughSMI_CMD, which nothing a dump holds answers._OSCs are evaluated and one block loads,CPU0PSD, 459 bytes. That makes 6,989 objects in 8,192 static slots, the arena's capacity read, not derived._CST. Committed whole, every processor's_CSTequals Linux's reading, 24 of 24: FFH MWAIT 0x0, 0x31 and 0x60, latencies 1, 253 and 1048. Queried first and committed with the offer, as ToyOS's rule does, C2 and C3 fall to I/O ports. The same holds of Intel's bits 3, 4, 8 and 9 alone. That is the note on Design: idle CPUs enter the deep idle states the firmware declares #870._STA. 53 equal Linux's and 3 differ:_OSI ("Processor Aggregator Device"), a feature group the owner ruled ToyOS answers as Windows does, no (byte scan).SMI_CMDare firmware calls; the kernel refuses none.Scope, and\_S5is 5.\_SB._OSCis negotiated, and its processors name none._STA, runs 12_INIand refuses nothing. Before this round it refused 9_STA: the 8 interrupt links' port write and the battery's read of the controller's space._REG(3, 1)and_PSRare refused, which the dump could not show: see Metal._STA. 57 equal Linux's, 27 differ and none is refused (49, 27 and 8 before):/dev/memrefused again; two on RAM; one on a port the dump does not hold._CST. All 16 equal Linux's registers, with the two latencies exchanged: ours C2 400 µs and C3 350, Linux's C2 350 and C3 400. Each of the 16_CSTbodies gives 400 before 350 in its bytes (byte scan). So the interpreter reads the table as written, and the order Linux lists is its own.The negative control, round 4 (script, log). The round's production sources reverted onto its base, the merge
6c6016915, under637409f88's tests:lib/userbound/src/firmware.rs,lib/acpiserver-api/src/lib.rs, and the server's and interpreter's re-cited files.toyos-userboundreds,EXIT=101:the_8259s_edge_level_control_is_refused_though_nothing_declares_itfinds the PIT refused.acpiserverpasses,EXIT=0: its change is the power sources line's spelling, through a constant, to the same bytes, and the rest is comments.The negative control, round 5 (script, run). Each patch applied as a checked patch, the judge run on a copy of round 4's T14 readback, the tree restored and clean after each:
01523db5dreverted whole (patch):EXIT=1,battery_readred on the ruled refusal, as on the T14 (log);EXIT=1,battery_readred on the same line, so an unruled refusal still reds it (log).The negative control, round 6: the judges (script, exits). The script judged round 5's T14 readback, round 4's AMD readback, and four mutants of round 5's T14 log. It ran once with the fix at
5352b0901and once with the fix reverted whole as a checked patch (judge-fix.patch), and the tree was restored after. Each command wascargo test --test toyos-build -- --metal --metal-readback <dir> <rows>:acpi_tables_loaded acpi_server_events battery_readEXIT=0, 3 passed (log)EXIT=0acpi_tables_loadedEXIT=0(log)EXIT=0MemoryTyperefusal, of\_SB_.PC00.GFX0._INI(mutant)acpi_tables_loadedEXIT=1, "said the refusal the owner ruled of what he did not rule" (log)EXIT=0(log)HKEY._INI's path changed toHKEZ(mutant)acpi_tables_loadedEXIT=1, the same (log)EXIT=0(log)x9(mutant)acpi_tables_loadedEXIT=1, "refused other than … x2" (log)EXIT=0(log)_BSTrefusedMemoryTypeafter the power sources line (mutant)battery_readEXIT=1, "refused something of this machine's AML" (log)EXIT=0(log)The negative control, round 6: the server. The test is new, and so is the method it calls, so the change reverted does not build under it. The mutant below is the control.
The negative control, earlier rounds. For the earlier stages, the three library crates' sources at the branch's base,
main'scc52b456c, under its tests: every one fails to build, its tests naming the API this branch adds (log). For the three rulings, the round's production sources reverted onto the round's basecec32d058under this head's tests (log):toyos-userbound's tests fail to build (ProtectedPort), anda_method_refused_at_the_bound_gives_back_what_it_madepasses, because the end of a refused method the ruling asked was already the interpreter's; its control is the twor2mutants. The server's tests live in its sources, so the mutants stand for it.The mutants. 18 of the earlier stages, each a patch made by one edit, applied, run and reversed at
b38a1381e(run.log), all red withEXIT=101:SMI_CMDdenied; memory writes denied._REGbefore_INI; the bridge by_HIDdropped;_GLKignored.And 8 of the three rulings at
b3bfc9742(run.log, the guest one's, the patches and each log beside them), each red on the test named against it, the tree clean after each:PROTECTEDpassed (EXIT=101, the test round 4 renamedthe_8259s_edge_level_control_is_refused_though_nothing_declares_it); the server denying 0xC00 itself (EXIT=101,every_port_write_is_asked_of_the_kernel_and_made_or_refused_as_it_answers);EXIT=101); a refused evaluation's Mutexes kept held (EXIT=101), botha_method_refused_at_the_bound_gives_back_what_it_made;EXIT=101,a_device_with_no_ini_at_or_below_it_is_not_examined); a refused_STAclosing its subtree and a non-Integer one closing it (EXIT=101each,a_refused_ini_or_sta_is_counted_and_the_walk_goes_on);EXIT=1,acpi_press_across_a_lock_waitred, the server ended 101 ininitialise). Round 6 cut that test, and the same mutant now reds the module test that holds its claim (below).And 1 of round 6, at
17c2cb89a(patch, log):Server::loadedinitialising before it serves the latched press, applied as a checked patch and reversed, the tree clean after.cargo test -p acpiserver,EXIT=101:a_press_latched_during_the_load_powers_off_before_an_ini_that_waits_on_the_firmwarepanics with "the firmware held the Global Lock 1s after a take left it the pending bit", where the machine should have powered off first.And 1 of round 4, at
637409f88(patch, run):PROTECTEDemptied, the ELCR passed,EXIT=101onthe_8259s_edge_level_control_is_refused_though_nothing_declares_it, the tree clean after.Two of round 2's were green first and their tests changed: a refused
_STA's device had no child for the walk to skip (270e2a854), and a Mutex left held is acquired again without refusal, the interpreter's acquisitions being recursive, so the test now holds\_GL(a4111b4ca).Gates
Round 6 changes production code (
Server::loaded, threeKernelmethods,OWNmoved toacpiserver-api), so every gate ran again at its head17c2cb89a, after the merge ofmain's347e3313d(#874, #896). Each log is whole and ends in its exit (script, exits):cargo run -- --ci hostEXIT=0(log)cargo test -p acpiserverEXIT=0, 81 passed (log)cargo testEXIT=0, 61 of 61 (log)cargo run -- --build-onlyEXIT=0(log)cargo run -- --build-only --arch aarch64EXIT=0(log)5352b0901cargo test --test toyos-build -- --metal --metal-readback <dir> <rows>EXIT=0each (Checks)cargo test --test toyos-build -- --metal --metal-readback <dir>/t14 acpi_tables_loaded acpi_server_events battery_readEXIT=2, staged and no machine touched (log)The guest suite is one test shorter:
acpi_press_across_a_lock_waitis cut.Round 7 (
a785962f4) changes only the track's issue, so the host suite alone ran again:cargo run -- --ci host,EXIT=0(log).Why a guest test is cut.
acpi_press_across_a_lock_waitbooted QEMU, launched acpiserver, pressed the button and judged the order of the server'smain. That is an integration test of a userspace program, which Tests refuses (#890). A module test of the server now holds what it checked (S5):a_press_latched_during_the_load_powers_off_before_an_ini_that_waits_on_the_firmware, overServer<Scripted, Bytes>, which scripts each part of it:_INIwaits on that hold.The wait for the firmware's release, which clears a latched press's enable and puts every enable back after, is
sci::await_release's, which sci.rs's host tests already hold. The cut takes what only that test used:acpi_mediated'sheldarm,BootOptions::acpi_tablesand its-acpitableplumbing, and the countpower::acpi_tables_loadedanswered. The merge of858da4d42later removedpower::acpi_tables_loadeditself; see that section.Its history, for the record: the guest suite found the order wanting at
52e84f36b(log), andbac5ccca6set it. Once the walk became §6.5.1's search for_INI, the table had to declare\_SB.GLIfor the test to meet a waiting_INIat all (b3bfc9742).Why
acpi_countedis a guest program. It is a metal row's job, as the hold it was split from is, and runs under QEMU nowhere: both are listed among the binaries no QEMU guest runs. No type or host test reaches the T14's controller raising a query.No new dependency. Against main's tip the branch adds 4,751 lines and removes 893 (
git diff --shortstat origin/main...17c2cb89a). Round 4 added 221 lines and removed 136: 32 of production code added and 33 removed, of which all but 7 are comments; 136 of tests and 63; and 82 of issues and 40. Round 5 adds 77 and removes 6, none of production code: 13 of tests and 6, and 64 of issues. Round 6 adds 199 and removes 320: 103 of production code and its in-file tests and 33, the module test 29 of those added; 72 of tests and 214, the cut guest test and what only it used; and 24 of issues and 73.The merge of
main's858da4d42(#893, #863)c7f81824dmergesorigin/mainat858da4d42. #893's test sweep had cut or replaced some of the same ACPI tests this branch changed, and four files conflicted. Every hunk of both sides is accounted for below. #863 changes only issues and touched nothing here.tests/common/power.rs. Integration tests outside the kernel are cut, and unit and module tests hold what they checked #893 cutacpi_power_button,machine_shutdownandACPI_PRESSED, and this branch had leftacpi_power_buttonas it was (main's side also keptacpi_tables_loadedfor its T14 judge, which this branch rewrote), so it goes with main. Both sides cutacpi_press_across_a_lock_waitand its SSDT builder. This branch's change toacpi_tables_loaded, which now returns(), still applied, but withacpi_power_buttongone the function had no caller: this branch'sacpi_tables_on_metalreads each bench's blocks itself. Soacpi_tables_loadedgoes, along withACPI_TABLE,ACPI_S5_HANDEDandslp_typ_a, which only it used.ACPI_ARMEDstays because main's stop test reads it.tests/common/qemu.rs. Both sides removedBootOptions::acpi_tablesand its-acpitableplumbing. Integration tests outside the kernel are cut, and unit and module tests hold what they checked #893 also removedsecond_slot, which this branch never touched. The file is main's.tests/guest/rust-tests/src/bin/acpi_mediated.rs. Both sides removed theheldarm, and Integration tests outside the kernel are cut, and unit and module tests hold what they checked #893 replaced it with thepressarm thatacpi_press_is_one_sciruns. Main's arm is kept whole. This branch'sUnlistedCached→RegisterCachedrename is kept as well; it merged cleanly in two places in this file.tests/toyos.rs. One conflict, theRUST_SKIPcomment onacpi_mediated: both sides removedacpi_press_across_a_lock_waitfrom it, and main namesacpi_press_is_one_sciin its place. This branch's other hunks merged cleanly:acpi_hold/acpi_counted, theBenchjudges,ACPI_MEDIATED_SAIDand the cut test's dispatch.issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.mdmerged cleanly: Integration tests outside the kernel are cut, and unit and module tests hold what they checked #893's paragraph on the three T14 rows was added beside this branch's edits.The first guest run of the merge was red (log,
EXIT=1). Resolvingacpi_mediated.rsby taking main's whole file had also dropped this branch's two cleanRegisterCachedhunks. A fresh three-way merge of that file, which takes main's side only inside conflicts, put them back (diff), and the merge commit was amended before it was pushed.c7f81824dcargo run -- --ci hostEXIT=0(log)cargo test --test toyos-buildEXIT=0, 52 of 52, includingacpi_mediated_access,acpi_lock_given_back_on_one_cpu,acpi_supply_outlives_holder,acpi_press_is_one_sciand the fourmachine_shutdown_*(log)cargo run -- --build-onlyEXIT=0(log)cargo test --test toyos-build -- --metal --metal-readback <dir> acpi_tables_loaded acpi_server_events battery_readEXIT=2: images staged, no machine touched (log,request.txt, image hash)cargo test --test toyos-build -- --metal --metal-readback <dir>/t14 acpi_tables_loaded acpi_server_events battery_readEXIT=0(the orchestrator's runner records it; the log carries none), 3 passed, 0 failed, 1 boot; readback imageb54a97da…ce48, the staged one (judge, verdict, exit)All exits are in
exits.txt. The suite is 52 tests, not round 6's 61, because #893 cut tests on main.What the merge changes on metal. The rows this branch owns,
acpi_tables_loaded,acpi_server_eventsandbattery_read, keep their definitions and judges. The image they boot changes:tests/images/testcases.tomllosessymbolizeandtoyfetch(Integration tests outside the kernel are cut, and unit and module tests hold what they checked #893);sdk/stdchanges;fs.rs,process.rs,net/connection.rsand the newlaunch_route.rs.So the T14 rows are staged again. The AMD bench's row is not staged.
Metal
Round 3 ran
acpi_tables_loadedon both benches atb3bfc9742. The T14 was green (judge, readback). The AMD bench was red, exit 1 (log), on three findings, each now ruled and built:_REG(3, 1)and_PSRrefusedConfigWrite, a write to 00:00.0 offset 0xB8, the root complex's index (what says so, the AML). It stays refused, the row expects it, and the issue records its cost.test_rs_acpi_holdexited 101 waiting for a count the idle bench's controller never raised. The hold now waits on the server's power sources line, which that boot wrote at 6.738 s.By hand on round 3's readbacks, the new judge passes both. On the AMD bench, every refused line up to the power sources line carries the ruled
ConfigWrite, the census readsno _REG runwith that refusal alone, and the rest is as before. On the T14, the ruledMemoryTyperefusal is met.Round 4's request, staged from the committed tree at
ddc243eaf(request.txt,metal-images.sha256):acpi_tables_loaded,acpi_server_eventsandbattery_readont14/: jobstest_rs_acpi_counted,test_rs_acpi_hold,reboot.acpi_tables_loadedonamd/: jobstest_rs_acpi_hold,reboot. The T14's image would red it:acpi_countedthere waits out the runner's bound on the idle controller, and the hold after it starts with none left.Round 4 ran that request at
ddc243eaf(639ce1374before the reword). The orchestrator ran both boots and judges (comment 6135803137).cargo test --test toyos-build -- --metal --metal-readback <dir>/amd acpi_tables_loaded, judgeEXIT=0, 1 passed, its readback's image the stagedf1bea98…(judge log, masked). The log reads 16 of 17 tables loaded, SSDT8 refused, and\_S5handed,SLP_TYPa=5, which the kernel keeps for PM1a 0x404. The refusals are the ruledConfigWrite's alone:_REGin the census,x1, and_PSR.acpi_tables_loadedandacpi_server_eventspassed, andbattery_readwas red, exit 1, on the ruled refusal its judge did not consult.01523db5dfixed that judge, and round 4's readback judged again atbf6a51068passed all three (log).What the T14's ruled write is, established this round (
round5/facts/):MemoryTypein unlisted firmware memory, inside the embedded controller's_INI, right after the server's firstSMI_CMDwrite (0xF5). The T14's own tables, run on the host through the interpreter with a call trace, make the same write byEC._INI→HKEY.WGIN→BPWC(1)→\_SB.SGOV(0x0902000D, 1)(trace, what the host answered).WGINcallsBPWC(1)whereWIN8is set and the SMI handler's answer toWGSV(1)is not zero;BPWCdrives the pad where that answer has bit 4 set and bit 7 clear (the bytes and their reading).\SBRG, the PCH's SBREG_BAR, is 0xFD000000, so the address is SBREG_BAR + Port ID 0x6E + 0xA70. Intel's 500 Series Chipset On-Package PCH datasheet vol. 2 (631120-002) §27.1 puts GPIO Community 0's registers at SBREG_BAR + its Port ID, and §27.1.114 names offset A70hPAD_CFG_DW0_GPPC_A_13, whose bit 0 isGPIOTXSTATE, "drive a level '1' to the TX output pad" (excerpts). The community's window, 0xFD6E0000 + 64 KiB, is one of four the GPIO controller\_SB.GPI0(INT34C5) lists in its_CRS. It holds every pad of GPP_A, GPP_B and GPP_T with their ownership, locks and GPE routing, and the datasheet sets "no read/write restriction to the Pad Configuration register set during host ownership". No UEFI memory type holds it: the kernel refused the write as in memory the firmware's map does not list.Benchalready expects. So the round changes no policy. Admitting it would mean admitting writes into the GPIO controller's window, every pad of the community, which a GPIO driver would claim. The owner ruled it stays refused, isolation deciding (6135767889).HKEY._INI's, is not detailed on the T14: the server prints the detail of a kind's first refusal only. On the host the tables reach writes at MCHBAR + 0x5F50, 0x5F54, 0x59A0 and 0x59A1 throughDYTC→SCPF→PL1S→CTCN, with MCHBAR assumed 0xFEDC0000. Intel's 11th Generation Core processor datasheet vol. 2 (631122) documents those offsets. It was fetched but is unread: the PDF is encrypted against the text extractor.Round 5's request, staged from the committed tree at
bf6a51068(request.txt,metal-images.sha256): the T14 alone,acpi_tables_loaded,acpi_server_eventsandbattery_readont14/.Round 5 ran that request at
bf6a51068(a99fa7c32before the reword), the orchestrator's boot and judge (comment 6135803137). The readback's image hashes to the staged one,f187cec….cargo test --test toyos-build -- --metal --metal-readback <dir>/t14 acpi_tables_loaded acpi_server_events battery_read, judgeEXIT=0, 3 passed, 0 failed (judge log, masked). The T14:\_S5handed,SLP_TYPa=7;_OSCs, query then commit, 0 refused, and loaded one block, of 459 bytes;_INIrun and the two the rulings name refused,MemoryTypex2, and_REG(3, 1)after them;Round 6's fixed judge on the same readback is green too (Checks).
Round 6's request, staged from the committed tree at
17c2cb89a(request.txt,metal-images.sha256): the T14 alone,acpi_tables_loaded,acpi_server_eventsandbattery_readont14/, since the server's start order is nowServer::loaded.Round 6 ran that request at
17c2cb89a, the orchestrator's boot and judge.cargo test --test toyos-build -- --metal --metal-readback <dir>/t14 acpi_tables_loaded acpi_server_events battery_read, judgeEXIT=0as the orchestrator recorded it, the log carrying none; 3 passed, 0 failed, 1 boot;verdict.txtpassed; the readback's image hashes to the staged7aa7fb2…(judge log, masked, exit, verdict and hashes). The T14:\_S5handed,SLP_TYPa=7;_OSCs, query then commit, 0 refused, and loaded one block at run time, 0 refused;_STA, 37_INIrun and the two the rulings name refused,MemoryTypex2, and_REG(3, 1)in one scope;Server::loadedandserve.The AMD bench is not booted at
17c2cb89a. The branch's delta in each production path but two is patch-identical (git patch-id) atddc243eaf, where round 4 booted it, and at17c2cb89a:kernel/src/arch/x86_64/acpi_mode.rs,lib/acpi/src,lib/userbound/src,sdk/abi/src,system/acpiserver/aml/src,acpi_hold.rsandacpi_counted.rs. The two that differ,system/acpiserver/srcandlib/acpiserver-api/src, carry round 6's change: the start order moved intoServer::loadedunchanged,Claim'slatched,ackandpower_offwith the bodiesmainandservehad, andOWNmoved. None of it branches on the machine, and the T14 boot above runs each changed line metal reaches; no metal row reachespower_off. The AMD row's new judge holds on round 4's AMD readback (fixed-amd,EXIT=0, Checks).Unsure
acpi_countedis mine, not ruled. The ruling moved the hold to the power sources line; this keepsacpi_server_events' count waited for, in a job of its own. The other answer is to cut the count from that row's claim.BTH0._INIwrite outside the map on the dump, with unrecorded reads answering 0. On the T14 the refused write is the controller's_INIdriving a GPIO pad's TX state, in a window the GPIO controller's_CRSlists. The owner ruled the same on it and on the hotkey device's MCHBAR write (6135767889), and the row expects both_INIs by path.HKEY._INI's refused write is on the T14. The host run puts it in MCHBAR; the T14's log does not say. The ruling covers it either way.Scriptedpowers off by panicking, so the module test reads the order off the panic's payload. A stop that happens never returns, so a test double that returned would model a stop that did not happen.\OSYSholds on the T14 before\_SB.PC00._INIruns. A dump taken under Linux holds the value after it.intel_idle.max_cstate=0, was never taken. The_CSTcomparison uses intel_idle's_ACPIstates, which are_CSTas Linux evaluated it.🤖 Generated with Claude Code
https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy