Skip to content

The AML interpreter: a machine's DSDT and SSDTs loaded into one namespace and evaluated, bounded in steps, depth, size and sum - #739

Merged
Japabu merged 13 commits into
mainfrom
claude/sleepy-cori-ejictl
Oct 7, 2026
Merged

Japabu merged 13 commits into
mainfrom
claude/sleepy-cori-ejictl

Conversation

@Japabu

@Japabu Japabu commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

What this changes

Adds userland/acpiserver/aml, package toyos-aml: the ACPI Machine Language interpreter, as a pure library. It loads a machine's DSDT and SSDTs into one namespace and evaluates its objects. It has no hardware access of its own, is #[no_std] and #[forbid(unsafe_code)], and has one dependency, toyos-acpi, for the table it takes. It also adds the crate's line in userland/Cargo.toml and its lock entry, the crate and its host exit in the track's interpreter stage, and one issue file.

  • A load takes a toyos_acpi::Table. Table::open already checks §5.2.6's header, length and checksum for every table, so the interpreter has no check of its own.
  • Loading and invocation are one walk (§5.4.2). A definition block's term list is interpreted as it is read, and a method body the same way when invoked. A name in an argument position resolves when reached, so an invocation takes exactly the arguments its method declares.
  • Hardware only through the caller's Host trait, in four spaces:
    • SystemMemory;
    • SystemIO, ports bounded at 0xFFFF;
    • PCI_Config, addressed by _ADR, _BBN and _SEG (§6.1.1, §6.5.5, §6.5.6), each refused rather than truncated when out of range. The device is the nearest one the region's scope is or lies in, so a region a method declares addresses the device the method is in. A device below bridges is on the bus the nearest bridge's Secondary Bus Number register names (PCI-to-PCI Bridge Architecture Specification 1.2, §3.2.5.4), each bridge read through the Host on the bus the one above it named. Each is asked first for its Header Type register: a function whose layout is not a PCI-to-PCI bridge's, or that is absent and answers all ones, has no such register and the region below it is refused. A register that answers a bus not above the bridge's own, as an unconfigured bridge's reset value of 0 does, names no bus and is refused. ACPI §6.5.4 is the reason for both and is cited at the rule. Nothing is kept from one access to the next;
    • EmbeddedControl, 256 bytes (§12).
  • Bounds:
    • every string, buffer and package is made by a constructor that bounds its size: 1 MiB, or 65,536 elements;
    • a step is charged for every 64 bytes of work, whether made, copied, compared, written as digits, walked bit by bit, or read to make something smaller; a NameString is charged for its bytes, and every walk of the namespace pays a step for each scope it climbs and each segment it looks up;
    • a per-interpreter Meter bounds at 16 MiB the sum of what a table sizes: every string's, buffer's and loaded table's bytes, every package element, every namespace node, and every package element's name not yet defined, with its segments. That is the meter's count. Each node and element carries a constant beside it that the meter does not count; the heap behind a full meter is measured below and recorded in the track.
  • A reference to a LocalX or ArgX does not hold it. Ref::Slot is a Weak: the frame alone holds its slots, so a reference whose method has exited names nothing (NotFound), as a reference to a named object that is gone does. A reference to a package element holds its package and is refused entry to a package or a named object. No reference owns another, so no chain of them forms and none is part of a cycle.
  • A store to a field unit writes slices of its own copy of the source, a character or a field's width at a time. The copy is charged and metered. A write runs firmware's own methods, which may store to the source; the store is of what the source held when it began.
  • Legacy constructs are accepted, by the owner's ruling of 2026-10-05: Processor parses by ACPI 6.3 Errata A, the last edition to define it (§20.2.5.2, §19.6.108; ObjectType 12, Table 19.36). Reserved bits and values do not refuse a table. A reserved address space, AccessType or UpdateRule is refused where an access needs its meaning.
  • Predefined objects answer as Windows does, by the owner's rulings:
    • \_OSI says yes to the 22 Windows version strings Microsoft publishes and no to everything else, the ACPI feature groups included. The source is Microsoft's page "How to Identify the Windows Version in ACPI by Using _OSI", which says "Windows supports _OSI only for the use of identifying the host version of Windows".
    • \_OS is "Microsoft Windows NT".
    • \_REV is 2.

Not yet:

  • Load, LoadTable and DataTableRegion. The T14's tables hold eight Load opcodes and one LoadTable; none runs while a table loads.
  • Access in the SMBus, SystemCMOS, PciBarTarget, IPMI, GeneralPurposeIO, GenericSerialBus, PCC, PlatformRtMechanism, FFixedHW, reserved and OEM spaces.
  • A PCI_Config region below a host bridge that names no _BBN, which is QEMU's: issues/qemus-interrupt-links-are-refused-by-the-aml-interpreter.md.
  • The _REG/_INI/_STA start-up sequence.
  • More than one invocation at a time.
  • Wiring into acpiserver.

Why

This is the interpreter stage of issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md. #713 placed that stage, run by the ACPI server and owning the T14's press issue; this PR names the crate in it and puts the host half of its exit before the T14 half, which stays open. It records the owner's three rulings of 2026-10-05 verbatim, and records his words on the clean room as the orchestrator's record of 2026-10-04 holds them. Under the stage it records what the T14 check found that a later stage would otherwise pay to find again, and the two weaknesses of the meter with their owner and exit.

  • Written from the ACPI Specification 6.5 alone, and from ACPI 6.3A for Processor. No other implementation was read. The T14's tables were inspected on their bytes with tools written for it.
  • Placement follows issues/the-tree-says-who-uses-each-thing.md: inside the ACPI server, its first user, as compositor/desktop and soundserver/mixer sit inside theirs.
  • No new gate. src/userlandhost.rs's survey gates the tests, and src/clippy.rs's nested run lints them.

The merge of origin/main at 257ebea2a (#713, ACPI stage 1)

d5c0062dc merges it; git show --remerge-diff d5c0062dc names two files and no other.

Round 5: what review round 4 came to

BLOCKER, offset 0x19 read as a Secondary Bus Number without asking whether the function is a bridge: holds, and fixed. Measured at 98ffa8753 on the T14's tables with every function the walk asks answering all ones, as an absent one does: all 14 tables loaded and the sweep refused no method, every region below such a function addressed on bus 255. At this head the same run refuses one SSDT and 50 methods, Rule.

  • Each bridge is asked for its Header Type first, offset 0x0E. Only layout 1, the low seven bits, has a Secondary Bus Number at 0x19; a function of any other layout is refused with that one register read and nothing accessed below it.
  • The bridge test's passing arm now answers layout 1 at both bridges, the upper with the multi-function bit set. It gains four refusals, each asserting that only the upper bridge's Header Type was read: layout 0 with 0x45 at 0x19, all ones at both, layout 2, and the multi-function bit alone. The first two are the review's.
  • The mutation the review names, the Header Type check removed, turns the test red, exit 101. So do eleven more and the whole source change reverted; the table is under Evidence.
  • A CardBus bridge, layout 2, is refused with the rest. It has a bus number at 0x19 too; see "What is unsure".

NOTEs.

  • The track's bullet has an exit a row can fail. On the T14 the server logs the load result of each of the 14 tables and a T14 row reads all 14; a table refused for a bridge's answer goes to the owner with that bridge's Header Type and bus registers as the firmware left them. Owner: the power-off stage. Review round 5 named one clause more, added at eea9ec35c: the same row reads that no method the server evaluated was refused for a bridge's answer, and one that was goes to the owner with the table refusal.
  • ACPI §6.5.4 holds, and is cited at the rule. Read from an Internet Archive capture of the 6.5 specification's chapter 6, taken 2025-10-28. OSPM "must guarantee that the following operation regions are always accessible: PCI_Config operation regions on a PCI root bus containing a _BBN object", SystemIO, and SystemMemory the address map returns. Of a region below a bridge it says "PCI Config Space Operation Regions are ready as soon the host controller or bridge controller has been programmed with a bus number", and that when the bridge "is turned off or disabled, PCI Config Space Operation Regions for child devices are no longer available". So firmware that reads below a bridge that is absent or unnumbered is outside what the specification lets it assume.
  • The sentence about shared names is withdrawn where it stood, in round 4's note below. It was never in an issue file; it is also in 98ffa8753's commit message, which is pushed and left as it is.
  • The QEMU issue's "nearest scope that names a _BBN" is "nearest Device".

Round 4: what each finding of review round 3 came to

Each was measured at 488a05a57 before it was changed: by the out-of-tree check, whose source and commands are in a comment below, and by the new tests run against the old source (EXIT=101, four tests red). The host was shared and loaded throughout; times are single runs of a release build.

BLOCKER 1, a field store borrowing its source while firmware runs: holds, and fixed. The review's construction, a _ADR method that stores to the buffer being stored, panicked at 488a05a57 with RefCell already borrowed, exit 101, for a Buffer and for a String source. Now both return, exit 0. The store writes from a copy of its own, so what is written is what the source held when the store began. a_field_store_writes_its_source_as_it_was_when_firmware_changes_it asserts the return, the bytes written and the source afterwards, for both. The String-to-field store of round 2 now peaks at 233,191 bytes of heap, from 167,680: the copy.

BLOCKER 2, an unresolved element's Path outside the meter: holds, and fixed. fill-lazy with 255-segment names at 488a05a57: 713,799,584 bytes of heap behind a full meter, across 631 tables, none of them kept. The review's arithmetic said about 770 MB. The element is now an Unresolved, metered at its own size and its segments'. The heap an interpreter held with its meter full, by what filled it:

filled with at 488a05a57 now peak now
buffers 16,776,232 16,776,232 16,842,340
package elements naming objects not yet defined, 255 segments each 713,799,584 15,953,360 20,394,381
the same, one segment each 63,533,264 19,549,520 20,090,705
field-unit names 41,091,752 41,091,744 50,062,620

tables_and_names_are_held_against_the_live_bound gains the case: seventeen tables of 1,020 such elements, refused by the fifteenth. What remains is the constant beside each node and element, 2.5 times the meter for field units, and the arena a refused load leaves at capacity, 24,115,888 bytes after one table of 204,000 field units was refused. Both are in the track beside "A refused evaluation keeps what it stored", with these numbers, the power-off stage as owner, and an exit.

BLOCKER 3, the secondary bus used unchecked and kept: decided both ways it was open.

  • A Secondary Bus Number that is not above the bridge's own bus names no bus below it and is refused, Rule. At 488a05a57 the new test's unconfigured bridge was read as bus 0 and the endpoint's field answered from there.
  • The answer is no longer kept. Every access asks the bridges and firmware's _ADR, _BBN and _SEG again, so a bridge renumbered since is seen and no issue is needed for one that is not. The cost on the T14's tables: the sweep of 1,543 methods made 358 PCI_Config reads where it made 331.
  • a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus has two bridges, the upper read on the _BBN bus and the lower on the bus the upper answered. Deleting .rev() turns it red, exit 101; so do taking any answer, taking an answer equal to the bridge's own bus, and asking for offset 0x18.
  • Round 5 adds the Header Type question and re-runs these on it; see "Round 5" and "The T14's tables".

BLOCKER 4, name resolution charged no step: holds, and wider than named. A loop of one operation, run until the step bound refused it:

the operation at 488a05a57 now
a lone name that is nowhere, from 250 scopes down 262,143 iterations, 915 ms 4,048, 3.3 ms
the same from 5,000 scopes down, built by Scope over an alias across 25 tables 262,143, 11.9 s 209, 3.4 ms
a path of 240 segments that resolves 262,143, 872 ms 4,002, 6.9 ms
a name behind 16,384 parent prefixes 262,143, 3.3 s 3,956, 19 ms
Notify of a device 240 scopes down, which writes its path out 209,715, 4.5 s 4,211, 24 ms

Every walk of the namespace now pays its caller a step for each scope climbed and each segment looked up: resolution, a definition's path, and a path written out for Notify, a refusal or a reference handed to the caller. A NameString is charged for its bytes where it is read. A path the caller of evaluate writes is the caller's and pays nothing.

Reading every operator for the same defect found it on six more paths, each an operator that reads all of a long object to make something short, and one copy that was not needed. Measured on objects of 64 KiB, a sixteenth of the largest:

the operation at 488a05a57 now
ToInteger of a string of zeros 209,715 iterations, 49.0 s 1,013, 78 ms
DerefOf of a string that names the root behind parent prefixes 174,762, 6.3 s 1,012, 20 ms
a long buffer stored to a buffer of one byte 174,762, 150 ms 1,010, 0.9 ms
the same to a buffer field of one bit 174,762, 288 ms 1,010, 0.9 ms
ToString of its first character 131,071, 114 ms 1,010, 1.2 ms
Mid of its first byte 116,508, 110 ms 1,009, 1.7 ms
_OSI of a long string, which was copied to be compared 209,715, 664 ms 87,381, 14 ms

Each of the six is now charged for what it reads. _OSI no longer copies, so it does no work its argument sizes and is charged nothing more. These six were not in the review; they are the same finding, and leaving them would have left "a step for every 64 bytes of work" false.

a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it holds twelve such loops, each counted by a named integer the loop increments, to no more iterations than the step bound over the charge. It does not rest on Bound being returned, which every loop reaches with or without its charge. Against the old source all twelve ran past their count; each charge removed alone turns its own case red.

The slowest evaluation among those measured at this head is the ToInteger loop, 78 ms. That is the slowest found, not a proof of the worst.

NOTEs.

  • Names taken from the local tables. RP03, PXSX and their _ADR are gone with the test that held them; the bridge test names BRG0, BRG1 and END0. Every NameSeg in the tests was then checked against the local tables and against main: LPCB, LPCR and three names the tests had invented that happened to occur there were renamed too. Review round 4 found five more NameSegs of the tests that occur in those tables as names and not on main, all generic words that identify nothing; the claim this note made of what is still shared was false and is withdrawn.
  • A PCI_Config region declared in a method. Fixed: the walk counts devices alone, so the region addresses the device its method is in, and no register of that device is read as a bridge's. The bridge test asserts it, and counting a method as a device turns it red. The two T14 methods "refused for a name inside them" are not this: each names an object none of the 14 tables defines.
  • "16 MiB in sum" is corrected above: it is the meter's count, with the measured heap beside it.

Earlier rounds' findings and what closed them are in the comments, at the heads they were measured on.

The T14's tables

Run by a check outside the tree, on the owner's local copy: the DSDT and the 13 SSDTs beside it. The interpreter ran against a host that answers reads from nothing: this machine's memory was not available, only its tables. Every read is answered zero except as each row says. The first row's result is the host's answer, not the table's: the DSDT's definition-block code reads a chipset-series word from SystemMemory and branches on it in two places, and at zero the two branches contradict. Every other row answers that one 16-bit word 2.

Six ways, each run at 98ffa8753 and at this head, every one exit 0:

every function the walk asks as a bridge answers at 98ffa8753 at this head
zero, and the chipset word zero too the DSDT refused, NotFound; nothing loads the same
zero at every register 13 of 14 load; 50 methods refused the same counts, refused now for the Header Type
as a bridge at reset: layout 1, secondary bus 0 13 of 14; 50 refused the same
as a configured bridge: layout 1, the bus after its own all 14; none refused the same
as absent: all ones at both registers all 14; none refused, regions addressed on bus 255 13 of 14; 50 refused
as a listing of the T14's functions under Linux reads all 14; none refused, 91 reads of a function the listing does not hold all 14; 42 methods refused

"Refused" counts methods in the sweep below, each for a bridge's answer. In every row that loads the DSDT, \_S0, \_S3, \_S4 and \_S5 each evaluate to a package of four, and the first element of \_S5 equals toyos_acpi::s5_slp_typ of the same DSDT.

The sweep. A byte scan found 2,175 method definitions, 1,543 without an argument, each evaluated once. Against configured bridges at this head: 1,068 gave a value; for 471 the scanned path named no object; 2 were refused for a name no table defines; 1 executed Fatal; 1 was refused for the SystemCMOS space. Those are round 3's counts exactly. The sweep made 409 PCI_Config reads where it made 358, the Header Type reads. While loading, the tables made 294 SystemMemory reads and 52 PCI_Config reads, no write, and 404 Global Lock calls.

The listing. The orchestrator's local reading of every PCI function of the T14 under Linux: its Header Type and its primary, secondary and subordinate bus bytes. Six of them are bridges, each with a secondary bus above its own. The host answered those four registers from it and zero for every other register of a listed function; a function it does not list answered all ones to every read. At this head the sweep asked 51 Header Types: 9 answered a bridge's layout, none another layout, and 42 were of a function the listing does not hold. Those 42 are the 42 refusals: each refused walk met a function the listing does not hold. At 98ffa8753 each of those was read as a secondary bus of 255 and its region addressed there.

What the refusal costs on the real machine is not shown by any of this. The listing is a reading under Linux, taken after Linux enumerated the buses, and Linux may have numbered a bridge the firmware left unnumbered: it does not show what the firmware leaves at boot. If the firmware leaves the one bridge that SSDT reads below present and numbered, all 14 tables load; if it leaves it unnumbered or hidden, that SSDT is refused. Which it is takes one read on the machine at boot. The track's bullet now owes it, with an exit.

What else this does not show: the tables against the machine's own memory. Every answer but those named was zero, so every branch on firmware state took its zero side.

Evidence

Head d5c0062dc, the merge of origin/main at 257ebea2a into eea9ec35c. The three gates below ran at this head. Everything else in this section and in "The T14's tables" was measured at 7d130196a, and "this head" there means it: git diff --stat 7d130196a d5c0062dc -- userland/acpiserver/aml is empty, so no source or test of the crate differs. Its one dependency does: #713 changed toyos-acpi, and the crate's tests are run again on it below.

cargo run -- --ci host: EXIT=0. The lines below are the gate's own, each whole, chosen from its log of 7,736 lines, which is in the orchestrator's scratch as aml-r7/ci-host-d5c0062dc.log.

15:33:56 === [ci] userland/acpiserver/aml
test result: ok. 27 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.72s
test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 17 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
15:33:59 [ci] userland/acpiserver/aml: cargo test --manifest-path userland/acpiserver/aml/Cargo.toml --target aarch64-apple-darwin
...
15:34:31 === [ci] nothing left in $TMPDIR or /tmp
15:34:31 [ci] nothing left in $TMPDIR or /tmp: every test took its scratch with it
15:34:31 [ci] Host: 77 step(s), all green
EXIT=0

The crate's host tests, cargo test --manifest-path userland/acpiserver/aml/Cargo.toml --target aarch64-apple-darwin: EXIT=0, log aml-r7/crate-d5c0062dc.log. 27 in evaluate, 22 in hostile, 18 in namespace, 17 in regions.

cargo run -- --build-only: EXIT=0, log aml-r7/build-only-d5c0062dc.log, 742 lines. Run because userland/Cargo.toml and its lock now hold both #713's server and this crate: the userland workspace resolves, acpiserver compiles and bin/acpiserver is in the image. git status --porcelain --ignore-submodules=none printed nothing after the three gates, so no build rewrote the lock.

Negative control and mutations of the bridge rule, at this head, each a checked patch, built, run and restored by one script, the tree clean after each; patches and the script's log in a comment below. Every build EXIT=0, and a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101 under each:

this round's whole source change reverted red
the Header Type not asked red
the Header Type read and not checked red
the multi-function bit counted as layout red
only layout 0 refused red
only an absent function refused red
a CardBus bridge counted red
the secondary bus asked before the Header Type red
any secondary bus taken; one equal to the bridge's own taken red, each
the bridge asked for its primary bus red
the bridges walked from the device up red
a method counted as a device red

Round 4's other mutations, at 98ffa8753, in the comments above and not run again: the source they mutate is unchanged since. A field store borrowing its source turns the field-store test red; an unresolved name held at no size, and its segments not counted, the meter test; each of the twelve charges removed alone, its own case of the walk test and no other.

Oracles. QEMU's own DSDT, whose boot logged ACPI: PM1a=0x604 SLP_TYPa=0; toyos_acpi::s5_slp_typ, a second reader of the same bytes, on QEMU's DSDT in the tree and on the T14's outside it; QEMU's CPU hotplug register block and the HPET specification's capability register, for the two methods run; the T14's tables; for the bridge rule, ACPI §6.5.4 read from the text, and a reading of the T14's own PCI functions under Linux. The Header Type layout and the Secondary Bus Number's offset are from the PCI-to-PCI Bridge Architecture Specification as I know it: I had no copy to read, and the section number the source cites for the register is unchecked.

Earlier rounds, at the heads they were measured on. Round 1's ten controls at be10f3bc, round 2's four mutations at 0af9f964, round 3's four at 488a05a57 and round 4's twenty-one at 98ffa8753 are in the comments above.

The guest suite: not run. No program depends on the crate, so no guest holds it.

Size. Against origin/main at 257ebea2a: 17 files, +5898/−8 — production 3435 (src/), tests 2309, issues, manifests and lock +154/−8. Round 5: 4 files, +79/−31; the named change after it: 1 file, +3.

What is unsure

  • The T14 result rests on one answered word and on bridges answered as configured, or as Linux left them. Whether these tables load on the machine, against its own memory and its bridges as its firmware leaves them, is unmeasured. If the firmware leaves the bridge one SSDT reads below unnumbered or hidden, that table is refused there, and whether a table real firmware ships may be refused for that is the owner's to rule: none of his three rulings covers it.
  • A region below a CardBus bridge is refused. Its layout has a bus number at the same offset. Legacy, by my reading of "zero legacy"; no table I have reads below one.
  • A field store is of its source as the store found it. Table 19.7 does not say what a store sees when firmware changes its source under it. My reading.
  • Every name costs a step more than it did, and a deep or long one more again. No T14 method the sweep ran reached the step bound.
  • _OSI and feature groups rest on one sentence of Microsoft's page. The T14's tables ask about one feature group, and what Windows answers to it was not measured on Windows.
  • AnyAcc: the narrowest naturally aligned unit that holds the whole field inside its region, else bytes. My reading; the T14's tables need at least the "inside its region" half.
  • Table 19.7's piecewise Buffer and per-character String field writes are implemented literally.
  • ToHexString of a buffer uses Table 19.7's two-digit form.
  • §19.3.5.4's XYZ example contradicts §19.6.20's definition. The code follows the definition, and the test says so where it asserts.
  • §19.6.88's SyncLevel rules are enforced as written. No T14 method the sweep ran was refused by one.
  • A reference to a package element, LocalX or ArgX stored into a package or a named object is refused. §19.3.5.8 allows it. No T14 load and no method the sweep ran was refused for it.
  • \_REV is 2, what Windows answers.

Anything a reader of main must not miss

  • The interpreter is not yet wired into acpiserver. Stage 1's server, userland/acpiserver, answers every embedded-controller query with nothing (userland/acpiserver/src/aml.rs), and the power-off stage is the first consumer of \_S5.
  • An interpreter that is full stays full, and its 16 MiB is its meter's count, not its heap: the track records both under the interpreter stage, with the power-off stage as their owner.

🤖 Generated with Claude Code

…ested

userland/acpiserver/aml (package toyos-aml) loads a machine's DSDT and
SSDTs into one namespace and evaluates its objects: the interpreter stage
of issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md.
It is written from the ACPI Specification 6.5 alone (chapter 20's
grammar, §5.3-5.5's namespace, loading and method execution, §19.3.5's
conversions and §19.6's operators), cited by section at each rule.

It sits inside the ACPI server, its first user by the track, as the
compositor's desktop and the soundserver's mixer sit inside theirs; the
server itself is stage 1's, on its own branch.

A definition block is interpreted as it is read, at load as a method is
at its invocation (§5.4.2), so a name in an argument position is resolved
when reached and a method invocation takes the arguments its method
declares. Hardware is reached only through the caller's Host: SystemMemory,
SystemIO, PCI_Config (by _ADR, _BBN and _SEG) and EmbeddedControl. Every
evaluation is bounded in steps, nesting, object size and time asked to
sleep, and malformed bytes are refused by name.

_OSI answers as the owner ruled, like Windows: yes to every Windows
version string Microsoft publishes, no to anything else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C9qc7GuMaHZ9xhKsZ57RVz

Japabu commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

Negative-control patches behind the body's table, each applied to userland/acpiserver/aml/ at be10f3bc.

How each control ran:

  1. Replace exactly one occurrence of the old text in the file (the script refuses a replacement that matches nothing, or more than once).
  2. Run cargo build --tests.
  3. Run cargo test --manifest-path userland/acpiserver/aml/Cargo.toml --target x86_64-unknown-linux-gnu.
  4. Restore the file with git checkout.

After the last control, git status --porcelain printed nothing.

(name, file, old, new)
("search-rules-off", "src/name.rs", "!self.root && self.up == 0 && self.segs.len() == 1", "false")
("preserve-reads-nothing", "src/field.rs", "0 => self.unit_read(f, u * w, w)?,", "0 => 0,")
("osi-says-linux", "src/lib.rs", '"Windows 2000",', '"Windows 2000",\n    "Linux",')
("string-to-integer-decimal", "src/object.rs", "char::from(c).to_digit(16)", "char::from(c).to_digit(10)")
("arg-reference-not-followed", "src/exec.rs",
   "match held {\n                    Object::Ref(r) => self.store_ref(&r, v),",
   "match held {\n                    Object::Uninit => self.store_ref(&Ref::Node(self.ns.root()), v),")
("depth-unbounded", "src/lib.rs", "MAX_DEPTH: u32 = 256;", "MAX_DEPTH: u32 = 1_000_000;")
("named-buffer-resized", "src/exec.rs", "*b.borrow_mut() = fit(n, len);",
   "*b.borrow_mut() = { let _ = len; fit(n.clone(), n.len()) };")
("load-not-rolled-back", "src/lib.rs",
   "for &id in f.created.iter().rev() {\n                    self.ns.remove(id);\n                }", "")
("index-field-offset-off-by-one", "src/field.rs",
   "self.write_field(index, Object::Int(offset))?;\n                let v",
   "self.write_field(index, Object::Int(offset + 1))?;\n                let v")
("checksum-unchecked", "src/lib.rs",
   "if t.iter().fold(0u8, |s, &b| s.wrapping_add(b)) != 0 {", "if false {")

Every control built (build EXIT=0) and failed the suite (test EXIT=101).

named-buffer-resized needed a second attempt. Its first form, n in place of fit(n, len), did not build: fit became an unused import under -D warnings. The form above keeps fit in use, builds, and goes red on store_converts_to_the_named_type_and_copy_object_does_not.


Generated by Claude Code

@Japabu

Japabu commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #739 at be10f3bc, round 1, against origin/main's .claude/agents/reviewer.md.

Net: 15 files, +4851/−0. Production: +3086 (src/), plus 17 lines of manifest and lock. Tests: +1748.

Merge: git merge-tree --write-tree origin/main be10f3bc exits 0 against 9de5d7df, 28 commits past the base 0613f93f, so it merges cleanly. Nothing has been measured on the merged tree.

I did not run the host gate or the mutation controls. My brief asked for both, but my role prompt says "you run no test and no build". The orchestrator owes both measurements; see BLOCKER 1 for the host gate. I checked the ten controls by reading only: each patch in the comment breaks exactly what its named test asserts. Their run log is the comment's, at be10f3bc.

BLOCKER

  1. Evidence: the PR body has no green cargo run -- --ci host. The only run is at be10f3bc as uid 0, and it ended EXIT=1, 2 of 76 step(s) red. issues/the-host-suite-assumes-a-user-that-is-not-root.md states that root is the cause "is read from the tests: the run was not repeated as a non-root user". To close this, the body needs a non-root run at this head (or at the merge), with its exit code and log.

  2. src/exec.rs:419: the interpreter refuses the DSDT of the QEMU that ToyOS tests on. toyos-acpi/fixtures/qemu-11.1.1/dsdt.bin is already in the tree: 8500 bytes, revision 1, the DSDT whose boot logged SLP_TYPa=0. It holds two ProcessorOps: 5B 83 38 "C000" 00 00000000 00 at byte 7450 and 5B 83 45 04 "C001" 01 … at byte 7508. term_in answers 5B 83 with Malformed, which refuses the whole table, so \_S5 never evaluates there. Under "Yes, one path", that makes power-off fail on every guest boot, and the body's claim "\_S5 evaluates to its package" is false for the only real firmware the tree holds.

    • This is a guess that one cheap measurement would have checked: load that fixture in a host test and evaluate \_S5 against the logged SLP_TYPa=0. That test is the independent oracle this high-risk change is missing.
    • The same applies to the T14's tables, which the owner holds locally ("a copy you hold"; "read only by a check run outside it"): run against them, and record the result in the body.
    • Every other strictness reading (any "must" refused as Malformed, NumElements overflow, the AsciiChar range, SyncLevel order, the header checksum) needs the same two measurements before it lands.
    • Whether a whole table is refused for one opcode a pre-6.4 firmware legitimately emits is the owner's call; see the readings below.
  3. src/object.rs:240-247 and src/exec.rs:1585-1597: the stated MAX_BYTES bound is false. ToHexString and ToDecimalString of a buffer, and to_str of one, build a string 3–4× the buffer's size with no bounded() check, and ToBuffer adds a byte to it.

    • ret(&[0x98, buffer(&int(0x100000), &[]), 0]) returns a string of about 3 MiB, where objects_are_bounded_in_size would require Err(Bound).
    • Nesting ToHexString(ToBuffer(…)) 20 levels deep, with the result stored to Debug (no copy), grows by a factor of 3 per level until allocation fails. That aborts the process: a crash from firmware bytes, not a refusal.
    • Fix: check bounded() on every object an operator constructs. The test above must be red today.
  4. src/exec.rs:143, src/field.rs:326-356, src/exec.rs:909-938: the step bound does not bound time, because one step can do up to 1 MiB of work.

    • Name a Buffer(0x100000){}, create a CreateField(B, 0, 0x800000, F) over it, and run While (One) { Store (F, Local0) }. Each step then walks 8 Mi bits one by one through bit/set_bit (read_buf_field), for about 2^20/4 iterations: on the order of 10^12 bit operations, which is a hang, not a refusal.
    • VarPackage(0x100000){} or Buffer(0x100000){} in a loop allocates and zeroes 24 MiB or 1 MiB per few steps.
    • The PR measured only stack depth, never the worst-case time of one evaluation. Fix: charge steps in proportion to bytes or elements allocated, copied or walked. Then measure that worst-case time, and add a hostile test for each construction above.
  5. src/object.rs:117-150 with src/exec.rs:1101-1106: total memory is unbounded, because MAX_BYTES limits a single object, not the sum.

    • Storing a fresh 1 MiB buffer into successive elements of a VarPackage(0x100000) (Store(Buffer(0x100000){}, Index(GPKG, Local0))) keeps all of them live: about 2^20/8 MiB, roughly 128 GiB, before the step bound. That ends in an allocation-failure abort, at load time or at evaluation.
    • Fix: a live-allocation budget per interpreter, refused by name, with a test that drives it.
  6. src/object.rs:148: references copy without nesting accounting, and are dropped recursively.

    • The loop While (One) { Store (Package(1){}, Local1); Store (Local0, Index (Local1, Zero)); Store (Index (Local1, Zero), Local0) } chains Ref::Elem through a fresh package on every iteration, about 7×10^4 links before MAX_STEPS.
    • When the frame drops, the chain unwinds recursively, Rc → Vec → Object → Rc, which I expect to overflow the 2 MiB test thread and the 8 MiB ToyOS main stack.
    • Add a test with returns(&that_loop), which must be Err(Bound(_)) and must not abort. Run it, and bound the length of reference chains (or drop iteratively) if it aborts.
  7. src/exec.rs:486-493 and src/namespace.rs:153: def_alias creates its node through ns.alias, never through define, so the alias is not pushed to f.created. Two wrong behaviours follow:

    • A refused table leaves its Aliases behind, which contradicts lib.rs:9 ("A load refused leaves the namespace without anything that table created").
    • An Alias defined inside a method survives the method's exit, against §5.5.2.3, so the second invocation fails with Exists.
    • Tests to add: invoking method("M", 0, &[0x06, name("\\SRC"), name("ALI")]) twice must give Ok both times; and after a refused SSDT holding Alias(\A, \B) followed by a collision, \B must be NotFound.
  8. src/field.rs:154-206: untrusted address words are truncated instead of refused, so hardware is addressed where firmware did not name it.

    • bus as u8 and segment as u16 silently wrap a _BBN of 0x100 to bus 0, and a _SEG above 0xFFFF the same way.
    • Address::Io passes any 64-bit port to the host, while EC and PCI are bounded by their form.
    • No test reaches the dev > 31 || fun > 7 refusal (field.rs:203) or the 4096-byte PCI bound (field.rs:157). Deleting the former, or changing <= 0x1000 to <= 0x10000, turns no test red.
    • Fix: refuse each of these by name. Each refusal needs a test in a_pci_config_region_addresses_its_devices_function that those patches turn red, including a _BBN of 0x100 and an I/O region past 0xFFFF.
  9. Mutations that I expect to pass on high-risk claims, which the implementer must run:

    • (a) Delete self.wait(timeout.saturating_mul(1000))?; at exec.rs:1420. A test that loops Wait(EVT, 0xFFFE) must turn red.
    • (b) Delete self.wait(n)?; at exec.rs:759. A test that loops Stall(0xFF) must turn red, checking that the sum of Event::Stall stays ≤ MAX_WAIT_US.
    • (c) At namespace.rs:57, drop && n.generation == id.generation. A test must keep a RefOf to a Name that a method created, let the method exit, create a new object (which reuses the slot), and require DerefOf of the old reference to be NotFound. That test must turn red.
    • (d) Delete bounded(out.len())?; at exec.rs:1574. A test of ConcatenateResTemplate over two 1 MiB templates must turn red.
    • None of these claims has a test that can fail today.
  10. src/lib.rs:296-312 duplicates something the tree already has. toyos_acpi::Table::open (toyos-acpi/src/lib.rs:131-157) already performs §5.2.6's length-and-checksum check, bounded by MAX_TABLE_LEN. The server will reach the DSDT through toyos_acpi::dsdt_address, so it would check the same header twice, in two places that must agree. The header should be checked once, in one declaration, and the interpreter should take the table as already checked.

NOTE

  • src/exec.rs:111 and src/name.rs:47: path_of_text and Path::absolute are two parsers of the same textual NameString. absolute could be path_of_text plus a check that the path is rooted.
  • src/exec.rs:1501 and src/exec.rs:1027: type_of and base(..).type_code() are two ways of following a reference to its type.
  • src/namespace.rs:138: generations wrap with wrapping_add, so once one slot has been reused 2^32 times, a stale NodeId names a live object again. A long-lived server makes that reachable; refuse the wrap or retire the slot.
  • src/exec.rs:1101: a package element can hold a Ref::Elem to its own package, which makes an Rc cycle. Each evaluation that builds one leaks it for the life of the server.
  • src/lib.rs:50-59: the library's contract does not state how much stack it needs. MAX_DEPTH × about 3.4 KiB (debug, per the PR body) is roughly 860 KiB, and every host has to provide that.
  • PR body: "This is the interpreter stage" — but issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md defines no interpreter stage and no exit for one, only "later stages".
  • The same issue still records only the orchestrator's black-box-oracle reading. It does not record the owner's later ruling (no clean-room machinery; writers never read another implementation), which this PR cites.

The implementer's readings

  • \_OS = "ToyOS": needs the owner. The ruling's stated purpose is that "the T14 then runs the path it was tested on", and Windows answers \_OS with "Microsoft Windows NT". Firmware branches on \_OS in its legacy path.
  • \_REV = 2: this is what Windows answers, so it is consistent with "Like Windows". It does not need the owner.
  • Feature-group strings answered Zero: needs the owner. The ruling covers version strings, "Linux" and "FreeBSD" only.
  • §5.4.2 "first object … a named control method" not enforced: leniency here, against strictness on every other "must", shows the strictness policy is a guess (BLOCKER 2). It does not need the owner on its own terms.
  • Table 19.7 piecewise field writes, AnyAcc as the narrowest natural unit, two-digit ToHexString, and the §19.3.5.4 XYZ discrepancy: each is a defensible reading of the specification and does not need the owner.
  • ProcessorOp refused, and strictness wherever the spec says "must": refuted in effect by QEMU's own DSDT (BLOCKER 2). Whether a whole table is refused for a deprecated or legacy construct is a policy the owner rules on.

SEND BACK

claude added 5 commits October 4, 2026 22:27
…DSDT loads

Review round 1, BLOCKERs 2 and 10.

The owner ruled on 2026-10-05 to accept what real firmware ships: "Parse
Processor and other legacy constructs real tables still contain, per
their last spec definition". ProcessorOp now parses by ACPI 6.3 Errata A,
the last edition to define it (§20.2.5.2, §19.6.108): a named object that
opens a scope, ObjectType 12 (Table 19.36), a Notify target. QEMU 11.1.1's
DSDT, already in the tree as toyos-acpi/fixtures/qemu-11.1.1/dsdt.bin,
holds two; it now loads, and its \_S5 gives SLP_TYPa 0, what its boot
logged.

A load takes a toyos_acpi::Table, whose open checks §5.2.6's length and
checksum for every table; the interpreter's own copy of that check goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C9qc7GuMaHZ9xhKsZ57RVz
…reference chains

Review round 1, BLOCKERs 3 to 8, and the NOTE on generations.

- Every string, buffer and package is made by one of the machine's
  constructors, which bound its size (1 MiB, or 65536 elements), charge a
  step for every 64 bytes of it, and hold it against the interpreter's
  Meter until it drops: what one interpreter holds live is bounded at
  16 MiB in sum. A conversion refuses an output too large before it
  builds it.
- A step is charged in proportion to work: bytes made, copied or
  compared, a bit walked in a buffer field, a byte written out as
  digits. The worst evaluation measured, a buffer field of 8 Mi bits
  read in a loop, ends refused in 110 ms (release) or 1.46 s (debug).
- A reference to a package element or to a LocalX or ArgX lives only in
  a LocalX or ArgX, which a method's exit clears: storing one into a
  package or a named object is refused, so no chain of references forms
  and no cycle outlives its evaluation.
- An Alias is an object its table or method created like any other: a
  refused load and a method's exit remove it.
- _BBN above 0xFF, _SEG above 0xFFFF, an _ADR naming no single function
  and a SystemIO port past 0xFFFF are refused, not truncated.
- A namespace slot whose generation would wrap is retired.
- \_OS is "Microsoft Windows NT", as the owner ruled on 2026-10-05.

Each construction the review names has a hostile test, red before this
commit: conversion output and proportional steps timed out at 120 s, the
live fill and the reference chain aborted, the Alias and address tests
failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C9qc7GuMaHZ9xhKsZ57RVz
…not two

Review round 1, BLOCKER 9. Mutation (c), the generation check dropped,
stayed green: the test's reusing object had gone with its method before
the old reference was followed, so the slot was dead either way. MAIN
now defines the reusing Name itself, alive when the reference is
followed.

Mutation (d), ConcatenateResTemplate's own bounded(), stayed green
because the constructor bounds every object it makes; the operator's
copy goes, and deleting the constructor's bound turns the test red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C9qc7GuMaHZ9xhKsZ57RVz
…stage

Review round 1, NOTEs and the implementer's readings.

The track records the owner's rulings of 2026-10-05 verbatim (\_OS,
_OSI feature groups, old opcodes) and the clean-room ruling as the
orchestrator's brief carried it, and gains an interpreter stage whose exit
a test and a run against the T14's tables can read.

- _OSI answers every ACPI feature group no, which is how Windows answers:
  Microsoft's page says "Windows supports _OSI only for the use of
  identifying the host version of Windows". The interpreter so claims
  nothing it does not carry.
- "Refuse only what is truly malformed": a multi-byte PkgLength's
  reserved bits, a zero-segment MultiNamePath, string bytes above
  AsciiChar, FieldFlags bit 7, SyncFlags' reserved bits and External's
  ArgumentCount no longer refuse a table. A reserved address space,
  AccessType or UpdateRule loads and is refused where an access needs its
  meaning.
- One parser reads a NameString written as text, for DerefOf and for the
  caller; one function follows a reference to its object, for ObjectType,
  SizeOf and Concatenate's type names.
- The contract states the stack an evaluation needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C9qc7GuMaHZ9xhKsZ57RVz

Japabu commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

Answer to review round 1, at head 0af9f964. origin/main (9de5d7df) was merged first. The evidence for each item is in the PR body.

BLOCKERs

  1. Fixed. cargo run -- --ci host run as a non-root user (uid 30033, useradd): EXIT=0, "Host: 76 step(s), all green". All five root-only tests pass.

  2. Fixed, by the owner's ruling of 2026-10-05 ("Accept what real firmware ships"). ProcessorOp parses by ACPI 6.3A §20.2.5.2/§19.6.108. QEMU 11.1.1's DSDT now loads, and \_S5 gives SLP_TYPa 0.

    • Reserved bits and values no longer refuse a table.
    • What still refuses a table is listed in the body; QEMU's table passes all of it.
    • T14: not run. Those tables are not in this environment.
  3. Fixed. Every string, buffer and package is made by a constructor that bounds it, and conversions refuse an oversized output before building it. The new test was red before the fix (timed out at 120 s).

  4. Fixed. Steps are charged per 64 bytes of work (bit walks per bit, digit output per input byte).

    • Worst case measured: 110 ms release, 1.46 s debug.
    • The hostile test timed out at 120 s before the fix.
  5. Fixed. A per-interpreter Meter holds every object from creation until it drops, capped at 16 MiB in sum. The fill test aborted with an allocation failure before the fix.

  6. Fixed, by prevention rather than a length bound. A reference to a package element, LocalX or ArgX can no longer be stored into a package or a named object, and a method's exit clears its LocalX and ArgX.

    • Your loop is now refused as Type.
    • Before the fix it aborted with "has overflowed its stack", as you predicted.
  7. Fixed. An Alias joins its creator's list of created objects. Both of your tests were added; they failed with Exists before the fix.

  8. Fixed. Refused by name:

    • a _BBN above 0xFF and a _SEG above 0xFFFF;
    • an _ADR naming no single function;
    • a PCI offset of 0x1000 or more;
    • a SystemIO port past 0xFFFF (Address::Io is now u16).

    Each has a case in an_address_out_of_its_form_is_refused.

  9. Run.

    • (a) red, EXIT=101.
    • (b) red, EXIT=101.
    • (c) green at first, because the test never reached a live reused slot. Fixed; now red, EXIT=101.
    • (d) your patch stays green: the constructor bounds every object, so the operator's bounded() was a redundant copy, and it is deleted. Deleting the constructor's bound instead is red, EXIT=101.
  10. Fixed. load takes a toyos_acpi::Table. The interpreter's own header check is gone.

NOTEs

  • One textual NameString parser: Path::text, with absolute built on it.
  • One way to follow a reference: followed, used by ObjectType, SizeOf and Concatenate.
  • Generation wrap: a slot that would wrap is retired, not reused.
  • Rc cycles: prevented, per BLOCKER 6.
  • Stack: the contract now states the stack requirement (≥ 1 MiB, from 256 × about 3.4 KiB measured).
  • Track stage: added, with an exit.
  • Clean-room ruling: recorded as the orchestrator's brief carried it, marked as not verbatim.

Readings

  • \_OS: "Microsoft Windows NT", per the owner's ruling.
  • Feature groups: answered no, per "Like Windows answers". Microsoft's page says Windows uses _OSI "only for the use of identifying the host version of Windows".
  • Old opcodes: accepted, per the ruling.

Generated by Claude Code

@Japabu

Japabu commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #739 at 0af9f964, round 2, against origin/main's .claude/agents/reviewer.md.

Net: 16 files, +5332/−0. Production: +3283 (src/). Tests: +1982. Manifests, lock and issue: +67.

Merge: git merge-tree --write-tree origin/main 0af9f964 exits 0 against f260e0b98, 16 commits past the merged 9de5d7df. None of those commits touches toyos-acpi/, userland/Cargo.*, src/userlandhost.rs or src/clippy.rs. Nothing has been measured on the merged tree; the merge queue's host will be that measurement.

Owner's rulings: the three 2026-10-05 quotes in the track match the orchestrator's record of them character for character.

Round 1 BLOCKERs

  1. CLOSED. A non-root cargo run -- --ci host ran at 0af9f964: EXIT=0, "Host: 76 step(s), all green".
  2. OPEN. The QEMU half is closed: qemus_dsdt_loads_and_its_s5_is_what_its_boot_logged loads the fixture with its two ProcessorOps. The T14 half is still unmeasured. The 2026-10-05 ruling sets its own condition, "Tested against QEMU's table in the tree and your T14 tables locally", and the stage this PR adds makes "its pull request recording the result" part of the exit. If this lands first, the exit names a merged PR that can never record it. The orchestrator holds those tables, so the check is cheap: load the DSDT and SSDTs and evaluate \_S5. It is the only reading that answers these questions:
    • whether a real table fits in MAX_STEPS per load;
    • whether a real table holds a DataTableRegion or an unsupported access at definition-block level, either of which refuses the whole table;
    • whether a real table reads hardware at load through an OperationRegion operand.
  3. CLOSED. a_conversion_is_bounded_in_what_it_constructs timed out before the fix (EXIT=124) and passes at this head.
  4. CLOSED. work_in_one_step_is_charged_in_proportion timed out before the fix (EXIT=124). The worst case was measured at 110 ms in release.
  5. CLOSED for the sum of live objects. what_is_held_live_is_bounded_in_sum aborted before the fix (EXIT=134). An allocation outside the meter is a new finding, below.
  6. OPEN. The body claims "No reference chains", but a Ref::Slot can outlive its frame and still be written through, which forms both a chain and a cycle. See the first BLOCKER below.
  7. CLOSED. an_alias_goes_with_what_created_it failed with Exists before the fix (EXIT=101).
  8. CLOSED. an_address_out_of_its_form_is_refused covers _BBN 0x100, _SEG 0x10000, device 32, function 8, PCI offset 0x1000 and port 0x10000. _BBN 0x100 was red before the fix.
  9. CLOSED. Mutations (a), (b), (c) and (d′) each built (EXIT=0) and failed the suite (EXIT=101). The finding behind (d), that the operator's bounded() merely repeated the constructor's own check, is accepted.
  10. CLOSED. load takes a toyos_acpi::Table, and the second header check is gone.

BLOCKER

  • userland/acpiserver/aml/src/exec.rs:446-449 and :1159-1162: a reference to a LocalX outlives its frame, and a store through it bypasses lasting, so the module header's "no chain of references forms and no cycle outlives its evaluation" is false.
    • How it happens: Return (RefOf (Local0)) hands the caller a Ref::Slot to the callee's slot. f.clear() empties that slot but does not free it, and nothing clears it again. Store (x, DerefOf (LocalN)) then writes into it through store_ref's Ref::Slot arm, which takes copy, not lasting.
    • The chain: with M being Method (M) { Return (RefOf (Local0)) }, run While (One) { Store (M (), Local2); Store (Local0, DerefOf (Local2)); Store (Local2, Local0) }. It costs about 13 steps a link, so about 8×10^4 links before MAX_STEPS. MAIN's f.clear() then drops them recursively, which is round 1's stack overflow by another path.
    • The cycle: Store (Local2, DerefOf (Local2)) makes a slot hold a reference to itself. That leaks one unmetered slot per iteration, for the life of the server.
    • Test to add: returns(&that_loop) must be Err(_) and must not abort. A second test must hold the self-cycle to a refusal.
    • The fix must also close the ArgX path, where a callee stores RefOf (Local1) through an Arg0 that refers to the caller's local.
  • userland/acpiserver/aml/src/field.rs:282: a String stored to a field unit allocates one field-sized Vec per character, all at once, before any charge and outside the Meter. That is L × up to 1 MiB.
    • Example: a field of 0x80_0000 bits over a SystemMemory region, and Store (ToHexString (Buffer (0x8000) {}), HUGE), asks for about 96 GiB. The process aborts on allocation failure: a crash from firmware bytes, the round 1 BLOCKER 3/5 class.
    • Test to add: that store must be Err(Bound(_)), and it must abort before the fix.
  • userland/acpiserver/aml/src/object.rs:49-51 and :79-82: the Meter stands behind the 16 MiB claim, but its invariant is held by a comment and a clamp.
    • give uses saturating_sub, so an under-count is hidden and the bound silently stops being true.
    • bits() hands out a resizable RefMut<Vec<u8>> with "never to resize" stated only in its doc comment. RefMut::map to [u8] makes a resize unrepresentable.
    • exec.rs:237's saturating_sub on the Global Lock count, and lib.rs:217's if let Ok that would silently leave \_OS undefined, are the same kind of code.
    • Fail fast on each instead (root CLAUDE.md, "Fail fast").

NOTE

  • userland/acpiserver/aml/tests/hostile.rs:296-299: a refused evaluation keeps what it stored. One hostile or buggy method therefore holds the 16 MiB budget for the server's lifetime, and every later evaluation that allocates is refused, \_S5's package included, and with it power-off ("Yes, one path"). Remove this compromise, or record it in the track with an exit.
  • userland/acpiserver/aml/tests/namespace.rs:203-210: the only real firmware run loads tables and reads one Name. Its assertion, element 0 == 0, is also what an all-zero package gives. Assert the whole _S5 package, and evaluate at least one QEMU method (a _CRS or _STA), so that real firmware code runs and not only gets parsed.
  • userland/acpiserver/aml/src/lib.rs:228-230: the "shorter than its header" branch cannot be reached, because Table::open refuses anything shorter than SDT_HEADER_LEN. Delete it.
  • issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md:70-77: the clean-room ruling is entered "not verbatim", but the orchestrator's record holds the owner's words, which can be quoted. Those words also retire the older "black-box oracles" reading above it, and that paragraph still stands.
  • PR body: the _OSI feature-group answer rests on one sentence quoted from an unnamed Microsoft page. Name the page in the body, and read from the T14's tables (BLOCKER 2) which feature groups that firmware asks about.
  • PR body: the host log is a curated excerpt. "test result: ok. 27 passed (evaluate)" is not a line cargo prints. The gate's final line and EXIT=0 carry the claim; attach the full log.

SEND BACK

Japabu and others added 2 commits October 7, 2026 14:27
…r counts tables and names

Review round 2 of #739, an outside review of the same head, and the run
against the T14's tables that round 1 left owed.

A reference to a LocalX or ArgX held its slot by an Rc, so one returned
from a method outlived the frame, could be stored through, and chained
or pointed at itself: 50,000 iterations of the review's self-store left
2.4 MB behind per evaluation. Ref::Slot is now a Weak: the frame alone
holds its slots, and a reference whose method has exited names nothing,
as a NodeId whose object is gone does. Frame::clear, which emptied the
slots to break cycles among them, has nothing left to break and goes.

A String stored to a field unit built one field-sized Vec per character
before writing any: 98,303 characters into a field of a mebibyte asked
for 96 GiB, and under a 1 GiB heap cap the process died with "memory
allocation of 1048576 bytes failed". write_field now hands write_units
slices of the source, which reads past a slice's end as zeros; nothing
is built, and the same store ends at the step bound with a peak heap of
168 KiB.

The Meter no longer hides an under-count (give panics), Data::bits
hands out a slice, and the Global Lock count and the predefined objects
fail fast in the same way.

The Meter counted strings, buffers and packages only. Measured at the
old head: 24 tables of a mebibyte, each kept by one method, were all
held, 25.2 MB and no refusal; one table naming 204,000 field units held
42.6 MB. A loaded table is now a metered Bytes, and every namespace
node is taken from the Meter at creation and given back at removal.

The T14's tables, read from outside the tree, found two things the
interpreter refused that firmware ships. An AnyAcc field took the
narrowest natural unit that held it even where that unit ran past its
region's end, which refused a 16-bit field in the last two bytes of a
13-byte region; it now takes such a unit only inside the region, else
bytes. A PCI_Config region reached its host bridge only from directly
below it; a device below a bridge is now on the bus the bridge's
Secondary Bus Number register names.

load's "shorter than its header" branch was unreachable behind
Table::open and goes. QEMU's DSDT is asserted whole and against
toyos_acpi::s5_slp_typ, and two of its methods run against the
registers they read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Round 3 negative control and mutations, at 488a05a57, in userland/acpiserver/aml/.

Each was a patch checked with git apply --check, applied, built with cargo test --manifest-path userland/acpiserver/aml/Cargo.toml --target aarch64-apple-darwin --no-run, run, and reversed with git apply -R in the same script. After the last, git status --porcelain --ignore-submodules=none printed nothing.

m0, the negative control: the whole source change reverted (git diff 4edffc16c 488a05a57 -R -- userland/acpiserver/aml/src, 520 lines; the tests stay as they are at 488a05a57). Build EXIT=0. Each new or changed test run alone with --exact:

test exit how
a_reference_to_a_local_ends_with_its_method 101 has overflowed its stack, SIGABRT
tables_and_names_are_held_against_the_live_bound 101 seventeen mebibytes of tables are held
an_access_type_sets_the_unit_and_anyacc_takes_the_narrowest_natural_one 101 the last two bytes of a 13-byte region are refused
a_pci_config_region_below_a_bridge_is_on_its_secondary_bus 101 the read is refused and no access is made
what_is_held_live_is_bounded_in_sum 0 asserts what was already so
qemus_dsdt_loads_and_its_s5_is_what_its_boot_logged 0 asserts what was already so
qemus_methods_run_against_the_registers_they_read 0 asserts what was already so

a_string_stored_to_a_field_is_bounded_as_it_is_written was not run on the reverted source: there it asks the allocator for 96 GiB on a shared machine. Its red arm is the same store in the out-of-tree harness (next comment) under an allocator capped at 1 GiB above what the interpreter held: memory allocation of 1048576 bytes failed, EXIT=134. On the fixed source the same run ends Bound: more steps than one evaluation may take with a peak heap of 167,680 bytes, EXIT=0.

m1, an AnyAcc unit may leave its region. Build EXIT=0, suite EXIT=101: an_access_type_sets_the_unit_and_anyacc_takes_the_narrowest_natural_one.

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 7611be298..e5512ed68 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -119,7 +119,7 @@ impl Machine<'_> {
                 .into_iter()
                 .find(|&w| {
                     let unit = f.bit / (8 * w);
-                    unit == (f.bit + f.len - 1) / (8 * w) && within((unit + 1) * w)
+                    unit == (f.bit + f.len - 1) / (8 * w) && within(0)
                 })
                 .unwrap_or(1),
             1 => 1,

m2, namespace nodes are not metered. Build EXIT=0, suite EXIT=101: tables_and_names_are_held_against_the_live_bound. (Its first form, take(0) and give(0), did not build: NODE became dead code under -D warnings.)

diff --git a/userland/acpiserver/aml/src/namespace.rs b/userland/acpiserver/aml/src/namespace.rs
index c9b91e7c5..684b46a85 100644
--- a/userland/acpiserver/aml/src/namespace.rs
+++ b/userland/acpiserver/aml/src/namespace.rs
@@ -140,7 +140,7 @@ impl Namespace {
             live: true,
         };
         let fresh = u32::try_from(self.nodes.len()).map_err(|_| Error::Bound("the namespace's node count"))?;
-        self.meter.take(NODE)?;
+        self.meter.take(NODE - NODE)?;
         let id = match self.free.pop() {
             Some(index) => {
                 let slot = &mut self.nodes[index as usize];
@@ -182,7 +182,7 @@ impl Namespace {
             };
             n.live = false;
             n.object = Object::Uninit;
-            self.meter.give(NODE);
+            self.meter.give(NODE - NODE);
             doomed.extend(core::mem::take(&mut n.children).into_values());
             // A slot whose generation would wrap is retired, so no stale
             // NodeId ever names a live object again.

m3, a bridge is asked for its primary bus. Build EXIT=0, suite EXIT=101: a_pci_config_region_below_a_bridge_is_on_its_secondary_bus.

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 7611be298..e6ccba023 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -203,7 +203,7 @@ impl Machine<'_> {
         let segment = u16::try_from(segment).map_err(|_| Error::Rule("a _SEG above 0xFFFF (§6.5.6)"))?;
         for &above in path.iter().skip(1).rev() {
             let b = self.function(above, segment, bus)?;
-            let secondary = Address::PciConfig { segment, bus, device: b.device, function: b.function, offset: 0x19 };
+            let secondary = Address::PciConfig { segment, bus, device: b.device, function: b.function, offset: 0x18 };
             bus = self.host.read(secondary, crate::Access::Byte).map_err(|d| Error::Host(d.0))? as u8;
         }
         let at = self.function(path.first().copied().unwrap_or(bridge), segment, bus)?;

The script's own log:

=== m0-the-whole-source-change-reverted.patch
build EXIT=0
hostile a_reference_to_a_local_ends_with_its_method EXIT=101
hostile tables_and_names_are_held_against_the_live_bound EXIT=101
regions an_access_type_sets_the_unit_and_anyacc_takes_the_narrowest_natural_one EXIT=101
regions a_pci_config_region_below_a_bridge_is_on_its_secondary_bus EXIT=101
hostile what_is_held_live_is_bounded_in_sum EXIT=0
namespace qemus_dsdt_loads_and_its_s5_is_what_its_boot_logged EXIT=0
namespace qemus_methods_run_against_the_registers_they_read EXIT=0
restored: 0 path(s) differ
=== m1-anyacc-unit-may-leave-its-region.patch
build EXIT=0
whole suite EXIT=101
test an_access_type_sets_the_unit_and_anyacc_takes_the_narrowest_natural_one ... FAILED
test result: FAILED. 15 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
restored: 0 path(s) differ
=== m2-nodes-are-not-metered.patch
build EXIT=101
whole suite EXIT=101
restored: 0 path(s) differ
=== m3-bridge-reads-its-primary-bus.patch
build EXIT=0
whole suite EXIT=101
test a_pci_config_region_below_a_bridge_is_on_its_secondary_bus ... FAILED
test result: FAILED. 15 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
restored: 0 path(s) differ
final: 0 path(s) differ
--- m2 again, in the form that builds
=== m2-nodes-are-not-metered.patch
build EXIT=0
whole suite EXIT=101
test tables_and_names_are_held_against_the_live_bound ... FAILED
test result: FAILED. 20 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.60s
restored: 0 path(s) differ
final: 0 path(s) differ

@Japabu

Japabu commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

The out-of-tree check behind the body's T14 and heap numbers, at 488a05a57: a cargo package outside the tree that depends on toyos-aml and toyos-acpi by path, built with cargo build --release on the development machine. <worktree> stands for this branch's worktree. It reads tables from a directory given on the command line; no table, and no byte or name from one, is in this comment or in the tree. Without --show it prints counts, lengths, exit kinds and the interpreter's own refusal texts.

Commands, each <cmd> > <log> 2>&1; echo EXIT=$?:

command exit
aml-check tables <the T14's tables> --mem=<one address>:2 0
aml-check tables <the T14's tables> (every read answered zero) 0, the DSDT refused
aml-check tables <QEMU 11.1.1's dsdt.bin> 0
aml-check chain, selfref, liveref, argref 0 each
aml-check strfield 1024 0; 134 at 0af9f964
aml-check retained 24, dense 0 each
aml-check fill-names, fill-lazy, fill-buffers 0 each

src/main.rs:

//! An out-of-tree check of `toyos-aml`: real tables read from a directory the
//! tree never holds, and the hostile constructions of review round 2, each
//! under an allocator that counts what the process holds live.
//!
//! Nothing here prints a byte of a table unless `--show` is given; without it
//! the output is counts, lengths, exit kinds and this interpreter's own
//! refusal texts.

#[path = "<worktree>/userland/acpiserver/aml/tests/common/mod.rs"]
mod common;

use std::alloc::{GlobalAlloc, Layout, System};
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};

use common::*;
use toyos_aml::{Access, Address, Denied, Error, Host, Interpreter, Value};

struct Counting;

static LIVE: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);
static CAP: AtomicUsize = AtomicUsize::new(usize::MAX);

unsafe impl GlobalAlloc for Counting {
    unsafe fn alloc(&self, l: Layout) -> *mut u8 {
        let now = LIVE.fetch_add(l.size(), Relaxed) + l.size();
        if now > CAP.load(Relaxed) {
            LIVE.fetch_sub(l.size(), Relaxed);
            return std::ptr::null_mut();
        }
        PEAK.fetch_max(now, Relaxed);
        unsafe { System.alloc(l) }
    }

    unsafe fn dealloc(&self, p: *mut u8, l: Layout) {
        LIVE.fetch_sub(l.size(), Relaxed);
        unsafe { System.dealloc(p, l) }
    }
}

#[global_allocator]
static A: Counting = Counting;

fn live() -> usize {
    LIVE.load(Relaxed)
}

/// A host that answers every read with zero and counts what it is asked.
#[derive(Default)]
struct Count {
    reads: [u64; 4],
    writes: [u64; 4],
    sleeps: u64,
    stalls: u64,
    notifies: u64,
    locks: u64,
    /// What a SystemMemory read at an address answers instead of zero.
    mem: BTreeMap<u64, u64>,
    /// A range of memory, (start, length), answering one value.
    range: Option<(u64, u64, u64)>,
}

fn space(a: Address) -> usize {
    match a {
        Address::Memory(_) => 0,
        Address::Io(_) => 1,
        Address::PciConfig { .. } => 2,
        Address::EmbeddedControl(_) => 3,
    }
}

impl Host for Count {
    fn read(&mut self, at: Address, w: Access) -> Result<u64, Denied> {
        if std::env::var("AML_TRACE").is_ok() {
            println!("    read {at:x?} {w:?}");
        }
        self.reads[space(at)] += 1;
        Ok(match at {
            Address::Memory(a) => self.mem.get(&a).copied().or_else(|| self.range.filter(|r| a >= r.0 && a < r.0 + r.1).map(|r| r.2)).unwrap_or(0),
            _ => 0,
        })
    }
    fn write(&mut self, at: Address, w: Access, v: u64) -> Result<(), Denied> {
        if std::env::var("AML_TRACE").is_ok() {
            println!("    write {at:x?} {w:?} {v:#x}");
        }
        self.writes[space(at)] += 1;
        Ok(())
    }
    fn sleep(&mut self, _: u64) {
        self.sleeps += 1;
    }
    fn stall(&mut self, _: u64) {
        self.stalls += 1;
    }
    fn timer(&mut self) -> u64 {
        0
    }
    fn notify(&mut self, _: &str, _: u64) {
        self.notifies += 1;
    }
    fn global_lock(&mut self, _: bool) -> Result<(), Denied> {
        self.locks += 1;
        Ok(())
    }
}

impl Count {
    fn line(&self) -> String {
        format!(
            "reads mem/io/pci/ec {:?} writes {:?} sleeps {} stalls {} notifies {} locks {}",
            self.reads, self.writes, self.sleeps, self.stalls, self.notifies, self.locks
        )
    }
}

fn open_load(i: &mut Interpreter, h: &mut dyn Host, t: &[u8]) -> Result<(), Error> {
    let signature: [u8; 4] = t[..4].try_into().unwrap();
    let table = toyos_acpi::Table::open(Image(t), 0, &signature, 0).map_err(|_| Error::Table("Table::open refused it"))?;
    i.load(h, &table)
}

/// An outcome with nothing of the table in it: the variant, and this
/// interpreter's own text where it carries one.
fn kind<T>(r: &Result<T, Error>) -> String {
    match r {
        Ok(_) => "Ok".into(),
        Err(Error::Malformed { why, .. }) => format!("Malformed: {why}"),
        Err(Error::NotFound(_)) => "NotFound".into(),
        Err(Error::Exists(_)) => "Exists".into(),
        Err(Error::Type(w)) => format!("Type: {w}"),
        Err(Error::Rule(w)) => format!("Rule: {w}"),
        Err(Error::Table(w)) => format!("Table: {w}"),
        Err(Error::Fatal { .. }) => "Fatal".into(),
        Err(Error::Bound(w)) => format!("Bound: {w}"),
        Err(Error::Unsupported(w)) => format!("Unsupported: {w}"),
        Err(Error::Host(_)) => "Host".into(),
    }
}

fn shape(v: &Value) -> String {
    match v {
        Value::Uninitialized => "Uninitialized".into(),
        Value::Integer(_) => "Integer".into(),
        Value::String(s) => format!("String[{}]", s.len()),
        Value::Buffer(b) => format!("Buffer[{}]", b.len()),
        Value::Package(p) => format!("Package[{}]", p.len()),
        Value::Reference(_) => "Reference".into(),
    }
}

// ---- a scan for method definitions, each then checked by evaluating it ----

fn pkg_len(b: &[u8], at: usize) -> Option<(usize, usize)> {
    let lead = *b.get(at)?;
    let follow = usize::from(lead >> 6);
    if follow == 0 {
        return Some((usize::from(lead & 0x3F), at + 1));
    }
    let mut len = usize::from(lead & 0x0F);
    for i in 0..follow {
        len |= usize::from(*b.get(at + 1 + i)?) << (4 + 8 * i);
    }
    Some((len, at + 1 + follow))
}

fn seg_ok(s: &[u8]) -> bool {
    s.len() == 4
        && matches!(s[0], b'A'..=b'Z' | b'_')
        && s[1..].iter().all(|c| matches!(c, b'A'..=b'Z' | b'0'..=b'9' | b'_'))
}

/// A NameString at `at`, resolved against `scope`: the segments, and where it ends.
fn name_at(b: &[u8], mut at: usize, scope: &[[u8; 4]]) -> Option<(Vec<[u8; 4]>, usize)> {
    let mut path: Vec<[u8; 4]> = scope.to_vec();
    if *b.get(at)? == b'\\' {
        path.clear();
        at += 1;
    } else {
        while *b.get(at)? == b'^' {
            path.pop()?;
            at += 1;
        }
    }
    let count = match *b.get(at)? {
        0x00 => {
            at += 1;
            0
        }
        0x2E => {
            at += 1;
            2
        }
        0x2F => {
            at += 2;
            usize::from(*b.get(at - 1)?)
        }
        _ => 1,
    };
    for _ in 0..count {
        let s = b.get(at..at + 4)?;
        if !seg_ok(s) {
            return None;
        }
        path.push(s.try_into().unwrap());
        at += 4;
    }
    Some((path, at))
}

fn text(path: &[[u8; 4]]) -> String {
    let segs: Vec<String> = path.iter().map(|s| String::from_utf8_lossy(s).into_owned()).collect();
    format!("\\{}", segs.join("."))
}

/// Every method a table seems to define outside any method, as (path, argument count).
fn methods(b: &[u8]) -> Vec<(String, u8)> {
    let mut out = Vec::new();
    let mut stack: Vec<(usize, Vec<[u8; 4]>)> = vec![(b.len(), Vec::new())];
    let mut i = 36;
    while i < b.len() {
        while stack.len() > 1 && stack.last().unwrap().0 <= i {
            stack.pop();
        }
        let (end, scope) = stack.last().unwrap().clone();
        let (op, ext, body) = match b[i] {
            0x5B => (b.get(i + 1).copied().unwrap_or(0), true, i + 2),
            o => (o, false, i + 1),
        };
        let skip = |n: usize| i + n;
        match (ext, op) {
            (false, 0x14) | (false, 0x10) | (true, 0x82..=0x85) => {
                let parsed = pkg_len(b, body).and_then(|(len, after)| {
                    let e = body + len;
                    (e <= end && e > after).then_some(())?;
                    let (path, named) = name_at(b, after, &scope)?;
                    Some((e, path, named))
                });
                match parsed {
                    Some((e, path, named)) if !ext && op == 0x14 => {
                        if let Some(&flags) = b.get(named) {
                            out.push((text(&path), flags & 7));
                        }
                        i = e;
                    }
                    Some((e, path, named)) => {
                        stack.push((e, path));
                        i = named + if ext && op == 0x83 { 6 } else if ext && op == 0x84 { 3 } else { 0 };
                    }
                    None => i += 1,
                }
            }
            // Buffers, packages and field lists hold no definition.
            (false, 0x11..=0x13) | (true, 0x81 | 0x86 | 0x87) => match pkg_len(b, body) {
                Some((len, _)) if body + len <= end => i = body + len,
                _ => i += 1,
            },
            (false, 0xA0 | 0xA1 | 0xA2) => i = pkg_len(b, body).map_or(i + 1, |(_, after)| after),
            (false, 0x0A) => i = skip(2),
            (false, 0x0B) => i = skip(3),
            (false, 0x0C) => i = skip(5),
            (false, 0x0E) => i = skip(9),
            (false, 0x0D) => i = b[i..].iter().position(|&c| c == 0).map_or(b.len(), |n| i + n + 1),
            (false, 0x08) => i = name_at(b, body, &scope).map_or(i + 1, |(_, after)| after),
            _ => i += 1,
        }
    }
    out
}

/// Every string a table passes straight to `_OSI`.
fn osi_strings(b: &[u8]) -> Vec<Vec<u8>> {
    let mut out = Vec::new();
    for i in 0..b.len().saturating_sub(5) {
        if &b[i..i + 4] == b"_OSI" && b[i + 4] == 0x0D {
            if let Some(n) = b[i + 5..].iter().position(|&c| c == 0) {
                out.push(b[i + 5..i + 5 + n].to_vec());
            }
        }
    }
    out
}

fn count_op(b: &[u8], a: u8, c: u8) -> usize {
    b.windows(2).filter(|w| w[0] == a && w[1] == c).count()
}

fn tables(dir: &str, show: bool, mem: &BTreeMap<u64, u64>) {
    let range = std::env::var("AML_RANGE").ok().map(|s| {
        let v: Vec<u64> = s.split(":").map(|x| u64::from_str_radix(x, 16).unwrap()).collect();
        (v[0], v[1], v[2])
    });
    let mut names: Vec<(u32, String)> = Vec::new();
    for e in std::fs::read_dir(dir).unwrap() {
        let n = e.unwrap().file_name().into_string().unwrap();
        if n == "DSDT" || n == "dsdt.bin" {
            names.push((0, n));
        } else if let Some(k) = n.strip_prefix("SSDT").and_then(|k| k.parse::<u32>().ok()) {
            names.push((k, n));
        }
    }
    names.sort();
    let mut i = Interpreter::new();
    let base = live();
    let (mut bytes_total, mut loaded, mut refused) = (0usize, 0, 0);
    let mut all: Vec<Vec<u8>> = Vec::new();
    for (_, n) in &names {
        let t = std::fs::read(format!("{dir}/{n}")).unwrap();
        let mut h = Count { mem: mem.clone(), range, ..Count::default() };
        let before = live();
        let r = open_load(&mut i, &mut h, &t);
        if r.is_ok() {
            loaded += 1;
            bytes_total += t.len();
        } else {
            refused += 1;
        }
        println!(
            "load {} len {} -> {} | heap +{} | {} | bytes 5B20 {} 5B1F {} 5B88 {}",
            if show { n.as_str() } else { &n[..4] },
            t.len(),
            kind(&r),
            live().wrapping_sub(before) as isize,
            h.line(),
            count_op(&t, 0x5B, 0x20),
            count_op(&t, 0x5B, 0x1F),
            count_op(&t, 0x5B, 0x88),
        );
        if show {
            if let Err(e) = &r {
                println!("    {e:?}");
            }
        }
        all.push(t);
    }
    println!("tables {} loaded {} refused {} | loaded bytes {} | heap held by the interpreter {}", names.len(), loaded, refused, bytes_total, live() - base);
    {
        let d = &all[0];
        let t = toyos_acpi::Table::open(Image(d), 0, b"DSDT", 0).unwrap();
        let scanned = toyos_acpi::s5_slp_typ(&t);
        let ours = i.evaluate(&mut Count::default(), "\\_S5", &[]);
        let first = match &ours { Ok(Value::Package(p)) => p.first().cloned(), _ => None };
        let agree = matches!((&first, scanned), (Some(Value::Integer(a)), toyos_acpi::S5::SlpTyp(b)) if *a == u64::from(b));
        println!("differential: the first element of \\_S5 against toyos_acpi::s5_slp_typ of the same DSDT -> {}", if agree { "equal" } else { "DIFFERENT" });
    }

    let mut h = Count { mem: mem.clone(), range, ..Count::default() };
    for s in ["\\_S0", "\\_S3", "\\_S4", "\\_S5"] {
        let r = i.evaluate(&mut h, s, &[]);
        match (&r, show) {
            (Ok(v), true) => println!("evaluate {s} -> {v:?}"),
            (Ok(v), false) => println!("evaluate {s} -> Ok {}", shape(v)),
            _ => println!("evaluate {s} -> {}", kind(&r)),
        }
    }
    println!("    during those: {}", h.line());

    let mut asked: Vec<Vec<u8>> = all.iter().flat_map(|t| osi_strings(t)).collect();
    asked.sort();
    asked.dedup();
    let (mut yes, mut no, mut no_windows) = (0, 0, 0);
    for s in &asked {
        let r = i.evaluate(&mut h, "\\_OSI", &[Value::String(s.clone())]);
        let said = matches!(r, Ok(Value::Integer(v)) if v != 0);
        if said { yes += 1 } else { no += 1 }
        if !said && s.starts_with(b"Windows") {
            no_windows += 1;
        }
        if show {
            println!("    _OSI({:?}) -> {}", String::from_utf8_lossy(s), said);
        }
    }
    println!("_OSI: {} distinct strings asked; {} answered yes, {} answered no ({} of those begin \"Windows\")", asked.len(), yes, no, no_windows);

    let mut found: Vec<(String, u8)> = all.iter().flat_map(|t| methods(t)).collect();
    found.sort();
    found.dedup();
    let zero: Vec<&String> = found.iter().filter(|(_, a)| *a == 0).map(|(p, _)| p).collect();
    let mut tally: BTreeMap<String, usize> = BTreeMap::new();
    let mut h = Count { mem: mem.clone(), range, ..Count::default() };
    let before = live();
    let start = std::time::Instant::now();
    let mut slowest = std::time::Duration::ZERO;
    for p in &zero {
        let t = std::time::Instant::now();
        let r = i.evaluate(&mut h, p, &[]);
        slowest = slowest.max(t.elapsed());
        let k = match &r {
            Ok(v) => format!("Ok {}", shape(v).split('[').next().unwrap()),
            Err(Error::NotFound(n)) if n == *p => "NotFound: the scanned path itself (no such object, or the scan misread)".into(),
            Err(Error::NotFound(_)) => "NotFound: a name the method uses".into(),
            _ => kind(&r),
        };
        if show {
            println!("    {p} -> {k} {}", if let Ok(v) = &r { format!("{v:x?}") } else { String::new() });
        }
        *tally.entry(k).or_default() += 1;
    }
    println!("methods: {} definitions scanned, {} of them take no argument; evaluated each once in {:?} (slowest {:?}):", found.len(), zero.len(), start.elapsed(), slowest);
    for (k, n) in &tally {
        println!("    {n:5}  {k}");
    }
    println!("    during those: {}", h.line());
    println!("    heap after the sweep {:+} against before it; process peak {}", live() as isize - before as isize, PEAK.load(Relaxed));
    let r = i.evaluate(&mut h, "\\_S5", &[]);
    println!("evaluate \\_S5 after the sweep -> {}", kind(&r));
}

fn report(what: &str, r: Result<Value, Error>) {
    match &r {
        Ok(v) => println!("{what} -> Ok {}", shape(v)),
        Err(e) => println!("{what} -> {e:?}"),
    }
}

const ZERO: &[u8] = &[0x00];

/// Review round 2, first BLOCKER: `Return (RefOf (Local0))`, stored through.
fn chain() {
    let m = method("M", 0, &ret(&ref_of(&local(0))));
    let main = method(
        "MAIN",
        0,
        &while_(
            &int(1),
            &cat(&[&store(&name("M"), &local(2)), &store(&local(0), &deref(&local(2))), &store(&local(2), &local(0))]),
        ),
    );
    let (mut i, mut h) = loaded(&cat(&[&m, &main]));
    report("chain", i.evaluate(&mut h, "\\MAIN", &[]));
}

fn selfref() {
    let m = method("M", 0, &ret(&ref_of(&local(0))));
    let body = cat(&[
        &store(&int(0), &local(3)),
        &while_(
            &lless(&local(3), &int(50_000)),
            &cat(&[&store(&name("M"), &local(2)), &store(&local(2), &deref(&local(2))), &increment(&local(3))]),
        ),
    ]);
    let (mut i, mut h) = loaded(&cat(&[&m, &method("MAIN", 0, &body)]));
    let before = live();
    report("selfref", i.evaluate(&mut h, "\\MAIN", &[]));
    println!("selfref: heap {:+} bytes after one evaluation", live() as isize - before as isize);
    let before = live();
    report("selfref", i.evaluate(&mut h, "\\MAIN", &[]));
    println!("selfref: heap {:+} bytes after a second", live() as isize - before as isize);
}

/// A LocalX holding a reference to itself and two holding each other, in a
/// frame that is alive, fifty thousand times over.
fn liveref() {
    let body = cat(&[
        &store(&ref_of(&local(1)), &local(2)),
        &store(&local(2), &deref(&local(2))),
        &store(&ref_of(&local(4)), &local(5)),
        &store(&ref_of(&local(5)), &local(4)),
    ]);
    let main = cat(&[
        &store(&int(0), &local(3)),
        &while_(&lless(&local(3), &int(50_000)), &cat(&[&name("CYC"), &increment(&local(3))])),
    ]);
    let (mut i, mut h) = loaded(&cat(&[&method("CYC", 0, &body), &method("MAIN", 0, &main)]));
    for round in 0..2 {
        let before = live();
        report("liveref", i.evaluate(&mut h, "\\MAIN", &[]));
        println!("liveref: heap {:+} bytes after evaluation {round}", live() as isize - before as isize);
    }
}

/// The ArgX path: a callee stores a reference to its own LocalX through an
/// ArgX that refers to the caller's.
fn argref() {
    let callee = method("PUT", 1, &store(&ref_of(&local(1)), &arg(0)));
    let main = method(
        "MAIN",
        0,
        &while_(
            &int(1),
            &cat(&[
                &cat(&[&name("PUT"), &ref_of(&local(2))]),
                &store(&local(0), &deref(&local(2))),
                &store(&local(2), &local(0)),
            ]),
        ),
    );
    let (mut i, mut h) = loaded(&cat(&[&callee, &main]));
    report("argref", i.evaluate(&mut h, "\\MAIN", &[]));
}

/// Second BLOCKER: a String of 98,303 characters stored to a field of 1 MiB.
fn strfield(cap: usize) {
    let body = cat(&[
        &op_region("MEM", 0x00, &int(0), &int(0x10_0000)),
        &field("MEM", 0x01, &[unit("HUGE", 0x80_0000)]),
        &method("MAIN", 0, &store(&op1(0x98, &buffer(&int(0x8000), &[]), ZERO), &name("HUGE"))),
    ]);
    let mut h = Count::default();
    let mut i = Interpreter::new();
    open_load(&mut i, &mut h, &dsdt(&body)).unwrap();
    PEAK.store(live(), Relaxed);
    CAP.store(live() + cap, Relaxed);
    let r = i.evaluate(&mut h, "\\MAIN", &[]);
    CAP.store(usize::MAX, Relaxed);
    println!("strfield -> {} | peak heap during it {} | {}", kind(&r), PEAK.load(Relaxed), h.line());
}

/// The outside review's third finding, measured: what a load leaves held
/// that is no string, buffer or package.
fn retained(k: usize) {
    let mut h = Count::default();
    let mut i = Interpreter::new();
    let base = live();
    open_load(&mut i, &mut h, &dsdt(&[])).unwrap();
    // One method a table, the rest of its mebibyte Noops in that method.
    let mut held = 0usize;
    for n in 0..k {
        let nm = format!("M{n:03}");
        let pad = vec![0xA3u8; (1 << 20) - 36 - 16];
        let t = table(b"SSDT", 2, &method(&nm, 0, &pad));
        let r = open_load(&mut i, &mut h, &t);
        if r.is_ok() {
            held += t.len();
        }
        println!("retained: table {n} of {} bytes -> {} | heap held {}", t.len(), kind(&r), live() - base);
        if r.is_err() {
            break;
        }
    }
    println!("retained: {held} table bytes loaded, heap held {}", live() - base);
}

/// The densest namespace a table can ask for: field units, five bytes each.
fn dense() {
    let mut units = Vec::new();
    let alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    let mut n = 0usize;
    // Field lists of 4000 units each, every one under a device of its own so
    // that names repeat.
    let mut body = op_region("MEM", 0x00, &int(0), &int(0x10_0000));
    let mut dev = 0usize;
    while body.len() < (1 << 20) - 36 - 30_000 {
        units.clear();
        for u in 0..4000usize {
            let s = [b'A' + (u / (36 * 36)) as u8, alphabet[(u / 36) % 36], alphabet[u % 36], b'_'];
            units.push(cat(&[&s, &[0x08]]));
            n += 1;
        }
        let d = format!("D{:03}", dev);
        dev += 1;
        body.extend(device(&d, &field("\\MEM", 0x01, &units)));
    }
    let t = dsdt(&body);
    let mut h = Count::default();
    let mut i = Interpreter::new();
    let base = live();
    let r = open_load(&mut i, &mut h, &t);
    println!("dense: a table of {} bytes naming {} field units -> {} | heap held {}", t.len(), n, kind(&r), live() - base);
}

fn main() {
    let a: Vec<String> = std::env::args().collect();
    let show = a.iter().any(|x| x == "--show");
    match a[1].as_str() {
        "tables" => {
            let mem = a.iter().filter_map(|x| x.strip_prefix("--mem=")).map(|kv| {
                let (k, v) = kv.split_once(":").unwrap();
                (u64::from_str_radix(k, 16).unwrap(), u64::from_str_radix(v, 16).unwrap())
            }).collect();
            tables(&a[2], show, &mem)
        }
        "chain" => chain(),
        "selfref" => selfref(),
        "liveref" => liveref(),
        "argref" => argref(),
        "strfield" => strfield(a[2].parse::<usize>().unwrap() << 20),
        "retained" => retained(a[2].parse().unwrap()),
        "dense" => dense(),
        "fill-names" => fill_names(),
        "fill-lazy" => fill_lazy(),
        "fill-buffers" => fill_buffers(),
        _ => panic!("what?"),
    }
}

/// Loads tables made by `make` until one is refused, and says what the
/// process held at the last one that loaded and at its peak.
pub fn until_refused(what: &str, first: &[u8], make: impl Fn(usize) -> Vec<u8>) {
    let mut h = Count::default();
    let mut i = Interpreter::new();
    let base = live();
    open_load(&mut i, &mut h, &dsdt(first)).unwrap();
    PEAK.store(live(), Relaxed);
    let mut held = live() - base;
    for n in 0..400 {
        let t = make(n);
        let r = open_load(&mut i, &mut h, &t);
        drop(t);
        if r.is_err() {
            println!("{what}: table {n} -> {} | heap held at the last that loaded {held} | peak {} | held after the refusal {}", kind(&r), PEAK.load(Relaxed) - base, live() - base);
            return;
        }
        held = live() - base;
    }
    println!("{what}: 400 tables loaded, heap held {held}");
}

pub fn fill_names() {
    let digits = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    let units: Vec<Vec<u8>> = (0..4000usize)
        .map(|u| unit(std::str::from_utf8(&[b'A' + (u / 1296) as u8, digits[u / 36 % 36], digits[u % 36]]).unwrap(), 8))
        .collect();
    // Fifty-one devices a table, then fewer, so the last table that loads ends near the bound.
    until_refused("names", &op_region("MEM", 0x00, &int(0), &int(0x1000)), |t| {
        let per = if t == 0 { 40 } else { 1 };
        let devices: Vec<Vec<u8>> = (0..per).map(|d| device(&format!("D{:01}{d:02}", digits[t % 36] as char), &field("\\MEM", 0x01, &units))).collect();
        table(b"SSDT", 2, &devices.concat())
    });
}

pub fn fill_lazy() {
    // Packages of 255 names that resolve to nothing yet, four bytes each.
    until_refused("unresolved names in packages", &[], |t| {
        let digits = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        let per = if t == 0 { 600 } else { 20 };
        let elems: Vec<Vec<u8>> = (0..255).map(|_| b"ZZZZ".to_vec()).collect();
        let body: Vec<u8> = (0..per)
            .flat_map(|p: usize| def_name(&format!("P{}{}", digits[p / 36 % 36] as char, digits[p % 36] as char), &package(&elems)))
            .collect();
        table(b"SSDT", 2, &device(&format!("L{t:03}"), &body))
    });
}

pub fn fill_buffers() {
    until_refused("buffers", &[], |t| {
        let digits = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        let per = if t < 15 { 16 } else { 1 };
        let body: Vec<u8> = (0..per).flat_map(|p: usize| def_name(&format!("B{}{}", digits[t % 36] as char, digits[p % 36] as char), &buffer(&int(0x1_0000), &[]))).collect();
        table(b"SSDT", 2, &body)
    });
}

@Japabu

Japabu commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #739 at 488a05a57, round 3, against origin/main (f260e0b98, which the head contains) and its .claude/agents/reviewer.md.

Net: 17 files, +5587/−3. Production: +3323 (src/). Tests: +2143. Issues, manifests and lock: +121/−3. This round: 10 files, +366/−114.

Read, not run. Every finding below that says "by reading" has no measurement behind it yet; the implementer's run decides it.

Round 2 BLOCKERs

  1. CLOSED. A LocalX reference outliving its frame. On the round's source reverted, a_reference_to_a_local_ends_with_its_method exits 101 with has overflowed its stack; at the head it passes, and the out-of-tree selfref run went from +2,400,000 bytes an evaluation to +0. Ref::Slot is a Weak and ref_of is its only maker.
  2. CLOSED as filed, and reopened by what closed it: see the first BLOCKER below. The same store went from memory allocation of 1048576 bytes failed, EXIT=134, under a 1 GiB cap to Bound with a peak heap of 167,680 bytes.
  3. CLOSED, by reading: Meter::give is a checked_sub(..).expect, Data::bits hands out RefMut<[u8]>, drop_global is a checked_sub(..).expect, and the predefined objects are made under an expect.

Round 1 BLOCKERs still open at round 2

  • 2, the T14 half: CLOSED. The run is recorded with its commands and exits: all 14 tables load with one word answered (EXIT=0), the DSDT is refused NotFound with every read zero (EXIT=0), and \_S5's first element equals toyos_acpi::s5_slp_typ. The body's sums match the run's own log (294 SystemMemory reads, 51 PCI_Config reads, 404 lock calls, 1,068 values of 1,543). What it does not show is stated, and the track names who owes the run on the machine.
  • 6: CLOSED with round 2's first.

Privacy

Passes. The local tables were unpacked into scratch and removed again. Their header OEM IDs, OEM table IDs and creator IDs, and every string of seven characters or more in them, were searched for in the diff, every commit message, the body and all six comments: the only hits are Microsoft's published _OSI strings, Microsoft Windows NT, and generic words. No table length, no address, no machine path and no model or BIOS string is posted; the harness prints <one address> and takes the directory as an argument. One thing is taken from the tables without identifying anything, under NOTE.

BLOCKER

  • userland/acpiserver/aml/src/field.rs:302-312 — a store to a field now holds a Ref on its source's RefCell across the whole write, and the write can run firmware's code — a panic from untrusted bytes, which lib.rs's contract says never happens, and new this round: the earlier code copied its pieces before the first write.
    • How, by reading: write_units → unit_write → address → pci (field.rs:154) → named_int → invoke (exec.rs:263) runs _ADR, _BBN or _SEG when one is a method. A source that is a named Buffer is the same Rc as the named object (node_value hands it out uncopied). A store to that name inside the method reaches Data::replace (object.rs:94, from exec.rs:1173 or :1180), whose RefCell::replace panics while borrowed; a buffer field over it reaches Data::bits and panics the same way.
    • Test to add, which must return and not panic: Device (PCI0) { Name (_BBN, 0) Name (BUFF, Buffer (4) {}) Method (_ADR) { Store (Zero, BUFF) Return (Zero) } OperationRegion (CFG, PCI_Config, 0, 0x10) Field (CFG, ByteAcc) { FLD, 32 } Method (MAIN) { Store (BUFF, FLD) } }, evaluating \PCI0.MAIN; and the same with a String source.
  • userland/acpiserver/aml/src/exec.rs:321 — a package element that names an object not yet defined keeps its whole Path outside the Meter, and firmware chooses the path's size — so the heap behind a full meter is not the measured 63.5 MB but, by this arithmetic, about twelve times that.
    • stream.rs:140 reads up to 255 segments, 1,020 bytes, for one step; the element is held at ELEMENT, 24 bytes. The body's fill-lazy used one-segment names: 63,533,264 bytes over 699,050 elements is 91 an element, which is 24 + the Rc and Path + 4. At 255 segments that is about 1,108 an element, about 770 MB under the same full meter. Unmeasured.
    • It needs no loop: a table that defines no method is dropped after its load and gives its bytes back to the meter, while the packages it named keep the paths.
    • Send back to measure: fill-lazy with 255-segment names. Then either the element holds nothing firmware sizes (a table offset to parse the name from when read, with the table held and metered) or the path's bytes are taken from the meter. tables_and_names_are_held_against_the_live_bound gains the case.
    • My judgement of the nominal meter, as asked: a fixed overhead per node or per element (the measured 2.5× and 3.8×) is a constant factor, a weakness to record and not a blocker; bytes whose count firmware chooses are what the meter exists for, and one kind of them escaping it is a blocker. What remains after the fix — the constant factor, and the arena a refused load leaves at capacity (24,115,888 bytes held after dense was refused) — is recorded only in the body: it goes into the track beside "A refused evaluation keeps what it stored", with the numbers, an owner and an exit.
  • userland/acpiserver/aml/src/field.rs:204-210 — the bus below a bridge is whatever byte the Secondary Bus Number register answers, used unchecked and kept for the region's life — an access through a bridge that is not configured lands on another device.
    • The register resets to 0 (the specification the code cites, §3.2.5.4). At 0 the endpoint's _ADR is addressed on bus 0: for an _ADR of zero that is function 00:00.0, the host bridge, and a write goes there. r.pci.set then keeps that answer for every later access; the body lists "a bridge renumbered afterwards is not seen" as unsure and nothing in issues/ records it.
    • Decide it and test it: a secondary bus that is not above the bridge's own bus names no bus below it and is refused by name, or the choice made instead is recorded with its owner and exit. Whether the T14's tables then still load is the measurement.
    • Mutation that stays green, by reading: delete .rev() at field.rs:204. a_pci_config_region_below_a_bridge_is_on_its_secondary_bus has one bridge, so the order of the walk is never exercised; with two bridges it must turn red, the upper one read first on the _BBN bus and the lower one on the bus the upper answered.
  • userland/acpiserver/aml/src/namespace.rs:104-118 with stream.rs:140 — a name's resolution is work firmware sizes and no step is charged for it, so "a step for every 64 bytes of work" does not hold for the commonest operation and the body's worst case, 110 ms at 0af9f964, is neither at this head nor the worst.
    • A lone NameSeg that misses climbs every parent to the root for one step; a 255-segment path is parsed and walked for one step. Depth is the table's choice ("which a table chooses", the module header says): 250 by nesting inside MAX_DEPTH, and further by Scope over a deep path, bounded only by the node count the meter allows.
    • By reading, While (One) { CondRefOf (ZZZZ, Local0) } in a method 250 scopes down is about 349,000 iterations of 250 map lookups each; over a namespace built deeper by Scope chains across tables it is that times the depth. Unmeasured.
    • This predates the round; it meets the bar as round 1's BLOCKER 4 did, the same defect on a path that one left. pci's new walk charges a step a level; resolve and Cursor::name do not. Measure the construction in the harness, charge a step per scope climbed and per segment, and work_in_one_step_is_charged_in_proportion gains the case.

NOTE

  • userland/acpiserver/aml/tests/regions.rs:201-204 — the bridge and endpoint are named RP03 and PXSX with _ADR 0x001C0002; both NameSegs occur in the local tables and neither was on main. They are a chipset vendor's reference names and identify no machine, but the tree is to hold nothing taken from those tables: rename them.
  • userland/acpiserver/aml/src/exec.rs:655 with field.rs:189-209 — a PCI_Config region declared inside a method has the method's own node as its scope, so the walk reads offset 0x19 of the enclosing device as though it were a bridge and then refuses with NotFound for <method>._ADR. By reading; the same refusal stood before this round. Say whether the two T14 methods "refused for a name inside them" are this, and either address the nearest enclosing Device or list it under "Not yet".
  • PR body, "Bounds" — "16 MiB in sum" is the meter's count; the sentence that follows says so, and the figure after it is the one the second BLOCKER disputes. Correct it once measured.

SEND BACK

…dge names no bus

Review round 3 of #739. Each finding was measured at 488a05a before it
was changed, by the out-of-tree harness and by the new tests run against
the old source.

A store to a field held a borrow of its source across the write, and a
write to a PCI_Config field runs firmware's _ADR, _BBN and _SEG: one that
stores to the source panicked, "RefCell already borrowed". The store now
writes from a copy of its own, charged and held against the meter, so it
is of what the source held when it began.

A package element naming an object not yet defined kept its Path outside
the meter. Filled with 255-segment names the heap behind a full meter was
713,799,584 bytes. The element is now an Unresolved, metered at its own
size and its segments': the same fill holds 15,953,360, and one-segment
names 19,549,520 where they held 63,533,264.

The bus below a bridge was whatever byte its Secondary Bus Number
register answered, kept for the region's life. A register that answers a
bus not above the bridge's own, as an unconfigured bridge's 0 does, now
names no bus and is refused; and nothing is kept: every access asks the
bridges and firmware's methods again, so a bridge renumbered since is
seen. A region declared in a method addresses the device the method is
in: the walk counts devices alone.

A name cost one step however long it was and however far it was searched
for. Every namespace walk now pays a step for each scope climbed and each
segment looked up (resolve, create, path_of), and a NameString is charged
for its bytes. A lone name that is nowhere, looked for from 5,000 scopes
down in a loop, ran 11.9 s before the step bound and now runs 3.4 ms.

The same defect on the paths it left, found by reading every operator for
work a table sizes: a long buffer stored to a short one or to a buffer
field, ToString and Mid of a long buffer's first byte, ToInteger and
DerefOf of a long string. Each is charged for what it reads. ToInteger of
a 64 KiB string of zeros in a loop ran 49 s and now runs 73 ms. _OSI
copied its argument to compare it and no longer does.

The tests name nothing that the local T14 tables hold and main did not,
beyond what the specification itself names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu Japabu changed the title The AML interpreter, from the ACPI 6.5 specification, pure and host-tested The AML interpreter: a machine's DSDT and SSDTs loaded into one namespace and evaluated, bounded in steps, depth, size and sum Oct 7, 2026
@Japabu

Japabu commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Round 4 negative control and mutations, at 98ffa8753, in userland/acpiserver/aml. Each is a patch that git apply --check accepted, applied, built with cargo test --manifest-path userland/acpiserver/aml/Cargo.toml --target aarch64-apple-darwin --no-run, run as cargo test ... --test <file> -- --exact <test>, and reversed with git apply -R by the same script, which ends on a clean tree. m00 is git diff 98ffa8753 488a05a57 -- userland/acpiserver/aml/src: the whole source change reverted under the new tests. Every build is EXIT=0 and every test EXIT=101.

In a first run, on the commit this head amends, m11 and m13 did not build (EXIT=101: the crate denies an unused variable); they were rewritten to keep the name used, and are below as run.

The script's log:

head 98ffa8753, 0 path(s) differ; host load averages: 27.59 28.81 34.87
=== m01-the-bridges-are-walked-from-the-device-up
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' panicked at acpiserver/aml/tests/regions.rs:212:5:
  left: Err(Rule("a bridge's Secondary Bus Number is not above its own bus, and names no bus below it"))
 right: Ok(Integer(134))
restored: 0 path(s) differ
=== m02-any-secondary-bus-is-taken
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' panicked at acpiserver/aml/tests/regions.rs:220:9:
0x0: Ok(Integer(0))
restored: 0 path(s) differ
=== m03-a-secondary-bus-equal-to-the-bridges-own-is-taken
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' panicked at acpiserver/aml/tests/regions.rs:221:9:
  left: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 64, device: 0, function: 0, offset: 25 }, Byte)]
 right: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 25 }, Byte)]
restored: 0 path(s) differ
=== m04-a-method-counts-as-a-device
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' panicked at acpiserver/aml/tests/regions.rs:235:5:
  left: Err(Rule("a bridge's Secondary Bus Number is not above its own bus, and names no bus below it"))
 right: Ok(Integer(134))
restored: 0 path(s) differ
=== m05-the-bridge-is-asked-for-its-primary-bus
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' panicked at acpiserver/aml/tests/regions.rs:212:5:
  left: Err(Rule("a bridge's Secondary Bus Number is not above its own bus, and names no bus below it"))
 right: Ok(Integer(134))
restored: 0 path(s) differ
=== m06-a-field-store-borrows-its-source
build EXIT=0
regions a_field_store_writes_its_source_as_it_was_when_firmware_changes_it EXIT=101
thread 'a_field_store_writes_its_source_as_it_was_when_firmware_changes_it' panicked at acpiserver/aml/src/object.rs:103:26:
RefCell already borrowed
restored: 0 path(s) differ
=== m07-an-unresolved-name-is-held-at-no-size
build EXIT=0
hostile tables_and_names_are_held_against_the_live_bound EXIT=101
thread 'tables_and_names_are_held_against_the_live_bound' panicked at acpiserver/aml/tests/hostile.rs:416:53:
seventeen mebibytes of names are held: None
restored: 0 path(s) differ
=== m08-an-unresolved-names-segments-are-not-counted
build EXIT=0
hostile tables_and_names_are_held_against_the_live_bound EXIT=101
thread 'tables_and_names_are_held_against_the_live_bound' panicked at acpiserver/aml/tests/hostile.rs:416:53:
seventeen mebibytes of names are held: None
restored: 0 path(s) differ
=== m09-a-search-toward-the-root-pays-nothing
build EXIT=0
hostile a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it EXIT=101
thread 'a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it' panicked at acpiserver/aml/tests/hostile.rs:343:5:
        "a lone name that is nowhere, searched to the root",
restored: 0 path(s) differ
=== m10-a-segment-looked-up-pays-nothing
build EXIT=0
hostile a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it EXIT=101
thread 'a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it' panicked at acpiserver/aml/tests/hostile.rs:343:5:
        "a path of that many segments",
restored: 0 path(s) differ
=== m11-a-parent-prefix-climbed-pays-nothing
build EXIT=0
hostile a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it EXIT=101
thread 'a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it' panicked at acpiserver/aml/tests/hostile.rs:343:5:
        "parent prefixes, each a scope climbed",
restored: 0 path(s) differ
=== m12-a-definitions-path-pays-nothing
build EXIT=0
hostile a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it EXIT=101
thread 'a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it' panicked at acpiserver/aml/tests/hostile.rs:343:5:
        "a definition by a path of that many segments",
restored: 0 path(s) differ
=== m13-a-path-written-out-pays-nothing
build EXIT=0
hostile a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it EXIT=101
thread 'a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it' panicked at acpiserver/aml/tests/hostile.rs:343:5:
        "Notify, which names its device by its path",
restored: 0 path(s) differ
=== m14-a-namestring-is-not-charged-for-its-bytes
build EXIT=0
hostile a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it EXIT=101
thread 'a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it' panicked at acpiserver/aml/tests/hostile.rs:343:5:
        "a name behind parent prefixes",
restored: 0 path(s) differ
=== m15-a-store-to-a-buffer-is-charged-for-the-target-alone
build EXIT=0
hostile a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it EXIT=101
thread 'a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it' panicked at acpiserver/aml/tests/hostile.rs:343:5:
        "a long buffer stored to a short one",
restored: 0 path(s) differ
=== m16-a-store-to-a-buffer-field-is-charged-for-the-field-alone
build EXIT=0
hostile a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it EXIT=101
thread 'a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it' panicked at acpiserver/aml/tests/hostile.rs:343:5:
        "a long buffer stored to a buffer field of a bit",
restored: 0 path(s) differ
=== m17-tostring-is-charged-for-its-result-alone
build EXIT=0
hostile a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it EXIT=101
thread 'a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it' panicked at acpiserver/aml/tests/hostile.rs:343:5:
        "ToString of a long buffer's first character",
restored: 0 path(s) differ
=== m18-mid-is-charged-for-its-result-alone
build EXIT=0
hostile a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it EXIT=101
thread 'a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it' panicked at acpiserver/aml/tests/hostile.rs:343:5:
        "Mid of a long buffer's first byte",
restored: 0 path(s) differ
=== m19-tointeger-of-a-string-is-not-charged
build EXIT=0
hostile a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it EXIT=101
thread 'a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it' panicked at acpiserver/aml/tests/hostile.rs:343:5:
        "ToInteger of a long string of zeros",
restored: 0 path(s) differ
=== m20-derefof-a-string-is-not-charged
build EXIT=0
hostile a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it EXIT=101
thread 'a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it' panicked at acpiserver/aml/tests/hostile.rs:343:5:
        "DerefOf of a long string that names the root",
restored: 0 path(s) differ
=== m00-the-whole-source-change-reverted
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' panicked at acpiserver/aml/tests/regions.rs:220:9:
0x0: Ok(Integer(0))
regions a_field_store_writes_its_source_as_it_was_when_firmware_changes_it EXIT=101
thread 'a_field_store_writes_its_source_as_it_was_when_firmware_changes_it' panicked at acpiserver/aml/src/object.rs:94:26:
RefCell already borrowed
hostile tables_and_names_are_held_against_the_live_bound EXIT=101
thread 'tables_and_names_are_held_against_the_live_bound' panicked at acpiserver/aml/tests/hostile.rs:416:53:
seventeen mebibytes of names are held: None
hostile a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it EXIT=101
thread 'a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it' panicked at acpiserver/aml/tests/hostile.rs:343:5:
        "a lone name that is nowhere, searched to the root",
        "a path of that many segments",
        "a name behind parent prefixes",
        "parent prefixes, each a scope climbed",
        "a definition by a path of that many segments",
        "Notify, which names its device by its path",
        "a long buffer stored to a short one",
        "a long buffer stored to a buffer field of a bit",
        "ToString of a long buffer's first character",
        "Mid of a long buffer's first byte",
        "ToInteger of a long string of zeros",
        "DerefOf of a long string that names the root",
restored: 0 path(s) differ
final: 0 path(s) differ; host load averages: 34.58 35.60 36.58

The patches, m01 to m20:

m01-the-bridges-are-walked-from-the-device-up.patch

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 9d1f5f4f8..1f078f767 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -205,7 +205,7 @@ impl Machine<'_> {
         let mut bus = u8::try_from(bus).map_err(|_| Error::Rule("a _BBN above 0xFF (§6.5.5)"))?;
         let segment = u16::try_from(segment).map_err(|_| Error::Rule("a _SEG above 0xFFFF (§6.5.6)"))?;
         let Some((&device, bridges)) = below.split_first() else { return self.function(host, segment, bus) };
-        for &bridge in bridges.iter().rev() {
+        for &bridge in bridges.iter() {
             let b = self.function(bridge, segment, bus)?;
             let secondary = Address::PciConfig { segment, bus, device: b.device, function: b.function, offset: 0x19 };
             let answered = self.host.read(secondary, crate::Access::Byte).map_err(|d| Error::Host(d.0))? as u8;

m02-any-secondary-bus-is-taken.patch

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 9d1f5f4f8..b789161f0 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -212,7 +212,7 @@ impl Machine<'_> {
             // The register resets to 0, and a configured bridge's secondary
             // bus is above the bus the bridge is on: any other answer would
             // address a device that is not below this bridge.
-            if answered <= bus {
+            if false {
                 return Err(Error::Rule("a bridge's Secondary Bus Number is not above its own bus, and names no bus below it"));
             }
             bus = answered;

m03-a-secondary-bus-equal-to-the-bridges-own-is-taken.patch

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 9d1f5f4f8..db00963f4 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -212,7 +212,7 @@ impl Machine<'_> {
             // The register resets to 0, and a configured bridge's secondary
             // bus is above the bus the bridge is on: any other answer would
             // address a device that is not below this bridge.
-            if answered <= bus {
+            if answered < bus {
                 return Err(Error::Rule("a bridge's Secondary Bus Number is not above its own bus, and names no bus below it"));
             }
             bus = answered;

m04-a-method-counts-as-a-device.patch

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 9d1f5f4f8..8b0527b95 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -188,7 +188,7 @@ impl Machine<'_> {
         let mut host = r.scope;
         loop {
             self.step()?;
-            if matches!(self.ns.object(host), Some(Object::Device)) {
+            if true {
                 if self.ns.child(host, bbn).is_some() {
                     break;
                 }

m05-the-bridge-is-asked-for-its-primary-bus.patch

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 9d1f5f4f8..43835655e 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -207,7 +207,7 @@ impl Machine<'_> {
         let Some((&device, bridges)) = below.split_first() else { return self.function(host, segment, bus) };
         for &bridge in bridges.iter().rev() {
             let b = self.function(bridge, segment, bus)?;
-            let secondary = Address::PciConfig { segment, bus, device: b.device, function: b.function, offset: 0x19 };
+            let secondary = Address::PciConfig { segment, bus, device: b.device, function: b.function, offset: 0x18 };
             let answered = self.host.read(secondary, crate::Access::Byte).map_err(|d| Error::Host(d.0))? as u8;
             // The register resets to 0, and a configured bridge's secondary
             // bus is above the bus the bridge is on: any other answer would

m06-a-field-store-borrows-its-source.patch

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 9d1f5f4f8..61808e7f9 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -311,8 +311,7 @@ impl Machine<'_> {
                 (&int, int.len())
             }
             Object::Buf(b) | Object::Str(b) => {
-                let bytes = b.borrow().clone();
-                copy = self.bytes(bytes)?;
+                copy = b.clone();
                 held = copy.borrow();
                 match &v {
                     Object::Str(_) => (&held, 1),

m07-an-unresolved-name-is-held-at-no-size.patch

diff --git a/userland/acpiserver/aml/src/object.rs b/userland/acpiserver/aml/src/object.rs
index c7beaabaa..5cb7787dd 100644
--- a/userland/acpiserver/aml/src/object.rs
+++ b/userland/acpiserver/aml/src/object.rs
@@ -156,7 +156,8 @@ pub(crate) struct Unresolved {
 
 impl Unresolved {
     fn held(path: &Path) -> usize {
-        core::mem::size_of::<Unresolved>() + core::mem::size_of_val(path.segs.as_slice())
+        let _ = path;
+        0
     }
 }
 

m08-an-unresolved-names-segments-are-not-counted.patch

diff --git a/userland/acpiserver/aml/src/object.rs b/userland/acpiserver/aml/src/object.rs
index c7beaabaa..b6398a8f7 100644
--- a/userland/acpiserver/aml/src/object.rs
+++ b/userland/acpiserver/aml/src/object.rs
@@ -156,7 +156,8 @@ pub(crate) struct Unresolved {
 
 impl Unresolved {
     fn held(path: &Path) -> usize {
-        core::mem::size_of::<Unresolved>() + core::mem::size_of_val(path.segs.as_slice())
+        let _ = path;
+        core::mem::size_of::<Unresolved>()
     }
 }
 

m09-a-search-toward-the-root-pays-nothing.patch

diff --git a/userland/acpiserver/aml/src/namespace.rs b/userland/acpiserver/aml/src/namespace.rs
index d749ec868..ba28717b0 100644
--- a/userland/acpiserver/aml/src/namespace.rs
+++ b/userland/acpiserver/aml/src/namespace.rs
@@ -113,7 +113,6 @@ impl Namespace {
         let Some(mut at) = self.start(scope, path, toll)? else { return Ok(None) };
         if path.searches() {
             loop {
-                toll()?;
                 if let Some(found) = self.child(at, path.segs[0]) {
                     return Ok(Some(found));
                 }

m10-a-segment-looked-up-pays-nothing.patch

diff --git a/userland/acpiserver/aml/src/namespace.rs b/userland/acpiserver/aml/src/namespace.rs
index d749ec868..f58b28576 100644
--- a/userland/acpiserver/aml/src/namespace.rs
+++ b/userland/acpiserver/aml/src/namespace.rs
@@ -122,7 +122,6 @@ impl Namespace {
             }
         }
         for &seg in &path.segs {
-            toll()?;
             let Some(below) = self.child(at, seg) else { return Ok(None) };
             at = below;
         }

m11-a-parent-prefix-climbed-pays-nothing.patch

diff --git a/userland/acpiserver/aml/src/namespace.rs b/userland/acpiserver/aml/src/namespace.rs
index d749ec868..9ac937c16 100644
--- a/userland/acpiserver/aml/src/namespace.rs
+++ b/userland/acpiserver/aml/src/namespace.rs
@@ -98,8 +98,8 @@ impl Namespace {
             return Ok(Some(self.root()));
         }
         let mut at = scope;
+        let _ = &toll;
         for _ in 0..path.up {
-            toll()?;
             let Some(above) = self.parent(at) else { return Ok(None) };
             at = above;
         }

m12-a-definitions-path-pays-nothing.patch

diff --git a/userland/acpiserver/aml/src/namespace.rs b/userland/acpiserver/aml/src/namespace.rs
index d749ec868..03a205834 100644
--- a/userland/acpiserver/aml/src/namespace.rs
+++ b/userland/acpiserver/aml/src/namespace.rs
@@ -137,7 +137,6 @@ impl Namespace {
         let missing = || Error::NotFound(crate::name::text(path));
         let mut at = self.start(scope, path, toll)?.ok_or_else(missing)?;
         for &seg in parents {
-            toll()?;
             at = self.child(at, seg).ok_or_else(missing)?;
         }
         if self.node(at).is_some_and(|n| n.children.contains_key(last)) {

m13-a-path-written-out-pays-nothing.patch

diff --git a/userland/acpiserver/aml/src/namespace.rs b/userland/acpiserver/aml/src/namespace.rs
index d749ec868..1ee27a1bf 100644
--- a/userland/acpiserver/aml/src/namespace.rs
+++ b/userland/acpiserver/aml/src/namespace.rs
@@ -211,7 +211,7 @@ impl Namespace {
         let mut at = id;
         while let Some(n) = self.node(at) {
             let Some(p) = n.parent else { break };
-            toll()?;
+            let _ = &toll;
             segs.push(n.seg);
             at = p;
         }

m14-a-namestring-is-not-charged-for-its-bytes.patch

diff --git a/userland/acpiserver/aml/src/exec.rs b/userland/acpiserver/aml/src/exec.rs
index 0aa14d718..907b70f40 100644
--- a/userland/acpiserver/aml/src/exec.rs
+++ b/userland/acpiserver/aml/src/exec.rs
@@ -193,7 +193,7 @@ impl<'a> Machine<'a> {
     fn name(&mut self, c: &mut Cursor<'_>) -> Result<Path, Error> {
         let at = c.at;
         let p = c.name()?;
-        self.charge(c.at - at)?;
+        let _ = at;
         Ok(p)
     }
 

m15-a-store-to-a-buffer-is-charged-for-the-target-alone.patch

diff --git a/userland/acpiserver/aml/src/exec.rs b/userland/acpiserver/aml/src/exec.rs
index 0aa14d718..ed119f424 100644
--- a/userland/acpiserver/aml/src/exec.rs
+++ b/userland/acpiserver/aml/src/exec.rs
@@ -1206,7 +1206,7 @@ impl<'a> Machine<'a> {
                 // Table 19.7: a buffer that exists keeps its size.
                 let n = to_buf(&v, w)?;
                 let len = b.borrow().len();
-                self.charge(n.len().max(len))?;
+                self.charge(len)?;
                 b.replace(fit(n, len))
             }
             Object::Pkg(p) => match &v {

m16-a-store-to-a-buffer-field-is-charged-for-the-field-alone.patch

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 9d1f5f4f8..d5da6177d 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -392,7 +392,7 @@ impl Machine<'_> {
             _ => return Err(Error::Type("a store to a buffer field of an object that is not an integer, buffer or string")),
         };
         // The source is read whole, and the field written a bit at a time.
-        self.charge(src.len().saturating_add(usize::try_from(f.len).unwrap_or(usize::MAX)))?;
+        self.charge(usize::try_from(f.len).unwrap_or(usize::MAX))?;
         let src = fit(src, bytes_for(f.len)?);
         let mut d = f.data.bits();
         if f.bit.saturating_add(f.len) > (d.len() as u64).saturating_mul(8) {

m17-tostring-is-charged-for-its-result-alone.patch

diff --git a/userland/acpiserver/aml/src/exec.rs b/userland/acpiserver/aml/src/exec.rs
index 0aa14d718..a1f740572 100644
--- a/userland/acpiserver/aml/src/exec.rs
+++ b/userland/acpiserver/aml/src/exec.rs
@@ -1703,7 +1703,6 @@ impl<'a> Machine<'a> {
             }),
             0x9C => {
                 let b = to_buf(&src, w)?;
-                self.charge(b.len())?;
                 let n = self.int_arg(f, c)?;
                 let n = if n == w.ones() { usize::MAX } else { usize::try_from(n).unwrap_or(usize::MAX) };
                 self.new_str(b.iter().take(n).take_while(|&&x| x != 0).copied().collect())?

m18-mid-is-charged-for-its-result-alone.patch

diff --git a/userland/acpiserver/aml/src/exec.rs b/userland/acpiserver/aml/src/exec.rs
index 0aa14d718..86ad82402 100644
--- a/userland/acpiserver/aml/src/exec.rs
+++ b/userland/acpiserver/aml/src/exec.rs
@@ -1715,7 +1715,6 @@ impl<'a> Machine<'a> {
                     Object::Str(s) => (s.borrow().clone(), true),
                     o => (to_buf(o, w)?, false),
                 };
-                self.charge(data.len())?;
                 let start = usize::try_from(i).unwrap_or(usize::MAX).min(data.len());
                 let end = start.saturating_add(usize::try_from(n).unwrap_or(usize::MAX)).min(data.len());
                 let part = data[start..end].to_vec();

m19-tointeger-of-a-string-is-not-charged.patch

diff --git a/userland/acpiserver/aml/src/exec.rs b/userland/acpiserver/aml/src/exec.rs
index 0aa14d718..bc009be54 100644
--- a/userland/acpiserver/aml/src/exec.rs
+++ b/userland/acpiserver/aml/src/exec.rs
@@ -1696,7 +1696,6 @@ impl<'a> Machine<'a> {
             },
             0x99 => Object::Int(match &src {
                 Object::Str(s) => {
-                    self.charge(s.borrow().len())?;
                     int_of_text(&s.borrow(), w)?
                 }
                 o => to_int(o, w)?,

m20-derefof-a-string-is-not-charged.patch

diff --git a/userland/acpiserver/aml/src/exec.rs b/userland/acpiserver/aml/src/exec.rs
index 0aa14d718..129d11142 100644
--- a/userland/acpiserver/aml/src/exec.rs
+++ b/userland/acpiserver/aml/src/exec.rs
@@ -211,7 +211,6 @@ impl<'a> Machine<'a> {
 
     /// DerefOf of a String names an object by ASL text (§19.6.30), read whole.
     fn path_of_text(&mut self, s: &Bytes) -> Result<Path, Error> {
-        self.charge(s.borrow().len())?;
         Path::text(&s.borrow()).ok_or(Error::Rule("DerefOf of a String that is not a name (§19.6.30)"))
     }
 

@Japabu

Japabu commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

The out-of-tree check behind the body's round 4 numbers. It is round 3's check (its source is in the comment above that begins "The out-of-tree check behind the body's T14 and heap numbers") with the changes below, built with cargo build --release against 488a05a57 for the "before" column and against 98ffa8753 for the "after" one. It reads tables from a directory given on the command line; no table, and no byte, address or name from one, is in this comment or in the tree.

Commands, each <cmd> > <log> 2>&1; echo EXIT=$?, with the exit before and after:

command at 488a05a57 at 98ffa8753
aml-check fieldborrow, fieldborrow string 101 each, RefCell already borrowed 0 each
aml-check fill-lazy-long, fill-lazy, fill-names, dense 0 each 0 each
aml-check fill-buffers 0
aml-check miss 250, miss-chain 25, carets 16384, segs 240, notify 240 0 each 0 each
aml-check sized <case> 65536, for osi, bufstore, bitstore, tostring, mid, toint, derefstr 0 each 0 each
aml-check strfield 1024 0 0
aml-check tables <the T14's tables> (every read answered zero) 0, the DSDT refused
aml-check tables <the T14's tables> --mem=<one address>:2 0, one SSDT refused
aml-check tables <the T14's tables> --mem=<one address>:2 --bridges 0, all 14 loaded

The host was shared and loaded while these ran: load averages near 100 during the "before" runs and near 35 during the "after" ones. The times in the body are single runs under that load.

Changes to round 3's source, as diff:

61a62,64
>     /// Whether a bridge answers its Secondary Bus Number register as one
>     /// configured would: the bus after its own.
>     bridges: bool,
80a84
>             Address::PciConfig { bus, offset: 0x19, .. } if self.bridges && w == Access::Byte => u64::from(bus) + 1,
289c293
< fn tables(dir: &str, show: bool, mem: &BTreeMap<u64, u64>) {
---
> fn tables(dir: &str, show: bool, mem: &BTreeMap<u64, u64>, bridges: bool) {
310c314
<         let mut h = Count { mem: mem.clone(), range, ..Count::default() };
---
>         let mut h = Count { mem: mem.clone(), range, bridges, ..Count::default() };
348c352
<     let mut h = Count { mem: mem.clone(), range, ..Count::default() };
---
>     let mut h = Count { mem: mem.clone(), range, bridges, ..Count::default() };
381c385
<     let mut h = Count { mem: mem.clone(), range, ..Count::default() };
---
>     let mut h = Count { mem: mem.clone(), range, bridges, ..Count::default() };
396c400
<             println!("    {p} -> {k} {}", if let Ok(v) = &r { format!("{v:x?}") } else { String::new() });
---
>             println!("    {p} -> {k} {}", match &r { Ok(v) => format!("{v:x?}"), Err(e) => format!("{e:?}") });
565a570,572
>     if round4(&a) {
>         return;
>     }
572c579
<             tables(&a[2], show, &mem)
---
>             tables(&a[2], show, &mem, a.iter().any(|x| x == "--bridges"))
597c604
<     for n in 0..400 {
---
>     for n in 0..4000 {

Appended to it:

// ---- round 4 -------------------------------------------------------------

/// Packages of names that resolve to nothing, each name 255 segments long.
pub fn fill_lazy_long() {
    let long = cat(&[&[b'\\', 0x2F, 255], &b"ZZZZ".repeat(255)]);
    until_refused("unresolved 255-segment names in packages", &[], |t| {
        let elems: Vec<Vec<u8>> = (0..255).map(|_| long.clone()).collect();
        let body: Vec<u8> = (0..4).flat_map(|p: usize| def_name(&format!("P{p}"), &package(&elems))).collect();
        table(b"SSDT", 2, &device(&format!("L{t:03}"), &body))
    });
}

/// `While (One) { <op>  Increment (\CNT) }` in `\MAIN` at the bottom of `outer`
/// nested devices, after `setup` at the root: how far it got, and how long it took.
fn looped(what: &str, i: &mut Interpreter, h: &mut Count, main: &str) {
    let start = std::time::Instant::now();
    let r = i.evaluate(h, main, &[]);
    let took = start.elapsed();
    let cnt = i.evaluate(h, "\\CNT", &[]);
    println!("{what} -> {} | iterations {:?} | {:?}", kind(&r), cnt, took);
}

fn spin(op: &[u8]) -> Vec<u8> {
    method("MAIN", 0, &while_(&int(1), &cat(&[op, &increment(&name("\\CNT"))])))
}

fn cond_ref_of(n: &[u8]) -> Vec<u8> {
    cat(&[&[0x5B, 0x12], n, &local(0)])
}

/// `depth` devices nested one in the next, `inner` in the last.
fn nested(depth: usize, inner: &[u8]) -> Vec<u8> {
    let mut b = inner.to_vec();
    for d in (0..depth).rev() {
        b = device(&format!("N{d:03}"), &b);
    }
    b
}

fn nested_path(depth: usize) -> String {
    let segs: Vec<String> = (0..depth).map(|d| format!("N{d:03}")).collect();
    format!("\\{}", segs.join("."))
}

/// A lone NameSeg that names nothing, looked for from `depth` scopes down.
fn miss(depth: usize) {
    let (mut i, mut h) = (Interpreter::new(), Count::default());
    let body = cat(&[&def_name("CNT", &int(0)), &nested(depth, &spin(&cond_ref_of(b"ZZZZ")))]);
    open_load(&mut i, &mut h, &dsdt(&body)).unwrap();
    looped(&format!("miss from {depth} scopes down"), &mut i, &mut h, &format!("{}.MAIN", nested_path(depth)));
}

/// The same from a scope `tables * 200` deep, each table opening the last one's
/// deepest device through an alias at the root.
fn miss_chain(tables: usize) {
    let (mut i, mut h) = (Interpreter::new(), Count::default());
    open_load(&mut i, &mut h, &dsdt(&cat(&[&def_name("CNT", &int(0)), &device("A000", &[])]))).unwrap();
    for t in 0..tables {
        let last = t + 1 == tables;
        let tail = cat(&[&[0x06], &name("N199"), &name(&format!("\\A{:03}", t + 1))]);
        let inner = device("N199", &if last { spin(&cond_ref_of(b"ZZZZ")) } else { tail });
        let r = open_load(&mut i, &mut h, &table(b"SSDT", 2, &scope(&format!("\\A{t:03}"), &nested(199, &inner))));
        if r.is_err() {
            println!("miss-chain: table {t} -> {}", kind(&r));
            return;
        }
    }
    let path = format!("\\A{:03}.{}.N199.MAIN", tables - 1, &nested_path(199)[1..]);
    looped(&format!("miss from {} scopes down", tables * 200), &mut i, &mut h, &path);
}

/// A name behind `n` parent prefixes.
fn carets(n: usize) {
    let (mut i, mut h) = (Interpreter::new(), Count::default());
    let nm = cat(&[&vec![b'^'; n], b"ZZZZ"]);
    open_load(&mut i, &mut h, &dsdt(&cat(&[&def_name("CNT", &int(0)), &spin(&cond_ref_of(&nm))]))).unwrap();
    looped(&format!("a name behind {n} parent prefixes"), &mut i, &mut h, "\\MAIN");
}

/// A path of `depth` segments that resolves.
fn segs(depth: usize) {
    let (mut i, mut h) = (Interpreter::new(), Count::default());
    let body = cat(&[&def_name("CNT", &int(0)), &nested(depth, &[]), &spin(&cond_ref_of(&name(&nested_path(depth))))]);
    open_load(&mut i, &mut h, &dsdt(&body)).unwrap();
    looped(&format!("a path of {depth} segments"), &mut i, &mut h, "\\MAIN");
}

/// A Notify of a device `depth` scopes down.
fn notify_deep(depth: usize) {
    let (mut i, mut h) = (Interpreter::new(), Count::default());
    let op = cat(&[&[0x86], &name("^"), &int(0x80)]);
    let body = cat(&[&def_name("CNT", &int(0)), &nested(depth, &spin(&op))]);
    open_load(&mut i, &mut h, &dsdt(&body)).unwrap();
    looped(&format!("Notify of a device {depth} scopes down"), &mut i, &mut h, &format!("{}.MAIN", nested_path(depth)));
}

/// An operator that walks or copies an object of `size` bytes for a result of a few.
fn sized(case: &str, size: usize) {
    let big = buffer(&int(size as u64), &[]);
    let (setup, op): (Vec<u8>, Vec<u8>) = match case {
        // _OSI of a long string.
        "osi" => (def_name("BIG", &whole_string(&vec![b'A'; size])), cat(&[&name("\\_OSI"), &name("BIG")])),
        // A long buffer stored to a named buffer of one byte.
        "bufstore" => (cat(&[&def_name("BIG", &big), &def_name("SMAL", &buffer(&int(1), &[]))]), store(&name("BIG"), &name("SMAL"))),
        // The same to a one-bit buffer field.
        "bitstore" => (
            cat(&[&def_name("BIG", &big), &def_name("SMAL", &buffer(&int(1), &[])), &cat(&[&[0x8D], &name("SMAL"), &int(0), &name("BIT0")])]),
            store(&name("BIG"), &name("BIT0")),
        ),
        // ToString of one character of it.
        "tostring" => (def_name("BIG", &buffer(&int(size as u64), &vec![b'A'; size])), cat(&[&[0x9C], &name("BIG"), &int(1), &local(0)])),
        // Mid of one byte of it.
        "mid" => (def_name("BIG", &big), cat(&[&[0x9E], &name("BIG"), &int(0), &int(1), &local(0)])),
        // ToInteger of a string of zeros.
        "toint" => (def_name("BIG", &whole_string(&vec![b'0'; size])), cat(&[&[0x99], &name("BIG"), &local(0)])),
        // CondRefOf (DerefOf (a string that is no name)) refuses; a name of many segments does not.
        "derefstr" => {
            let text: Vec<u8> = std::iter::once(b'\\').chain(std::iter::repeat_n(b'^', size)).collect();
            (def_name("BIG", &whole_string(&text)), store(&deref(&name("BIG")), &local(0)))
        }
        _ => panic!("what?"),
    };
    let (mut i, mut h) = (Interpreter::new(), Count::default());
    // Name takes a DataObject alone, so a string made by an operator is made at load by a method.
    let body = match case {
        "osi" | "toint" | "derefstr" => {
            let made = &setup[1 + 4..];
            cat(&[&def_name("CNT", &int(0)), &def_name("BIG", &string("")), &store(made, &name("BIG")), &spin(&op)])
        }
        _ => cat(&[&def_name("CNT", &int(0)), &setup, &spin(&op)]),
    };
    let r = open_load(&mut i, &mut h, &dsdt(&body));
    if r.is_err() {
        println!("{case}: load -> {}", kind(&r));
        return;
    }
    looped(&format!("{case} over {size} bytes"), &mut i, &mut h, "\\MAIN");
}

/// Review round 3, first BLOCKER: a field store whose `_ADR` stores to the source.
fn fieldborrow(string: bool) {
    let src = if string { string_of("ABCD") } else { buffer(&int(4), &[]) };
    let body = device(
        "PCI0",
        &cat(&[
            &def_name("_BBN", &int(0)),
            &def_name("BUFF", &src),
            &method("_ADR", 0, &cat(&[&store(&int(0), &name("BUFF")), &ret(&int(0))])),
            &op_region("CFG", 0x02, &int(0), &int(0x10)),
            &field("CFG", 0x01, &[unit("FLD", 32)]),
            &method("MAIN", 0, &store(&name("BUFF"), &name("FLD"))),
        ]),
    );
    let (mut i, mut h) = (Interpreter::new(), Count::default());
    open_load(&mut i, &mut h, &dsdt(&body)).unwrap();
    report("fieldborrow", i.evaluate(&mut h, "\\PCI0.MAIN", &[]));
}

fn whole_string(b: &[u8]) -> Vec<u8> {
    cat(&[&[0x9C], &buffer(&int(b.len() as u64), b), &ones(), ZERO])
}

fn string_of(s: &str) -> Vec<u8> {
    string(s)
}

pub fn round4(a: &[String]) -> bool {
    let n = || a[2].parse::<usize>().unwrap();
    match a[1].as_str() {
        "fill-lazy-long" => fill_lazy_long(),
        "miss" => miss(n()),
        "miss-chain" => miss_chain(n()),
        "carets" => carets(n()),
        "segs" => segs(n()),
        "notify" => notify_deep(n()),
        "sized" => sized(&a[2], a[3].parse().unwrap()),
        "fieldborrow" => fieldborrow(a.get(2).is_some_and(|s| s == "string")),
        _ => return false,
    }
    true
}

@Japabu

Japabu commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #739 at 98ffa8753, round 4, against origin/main (df1a77221; the branch's merge base is f260e0b98, and git merge-tree of the two is clean) and its .claude/agents/reviewer.md.

Net: 17 files, +5845/−3. Production: +3418 (src/). Tests: +2291. Issues, manifests and lock: +136/−3. This round: 10 files, +433/−175; production +243/−148, tests +173/−25. The production growth is the charges, the Unresolved element and the bridge rule; accepted.

Read, not run. What I measured myself is the privacy search and the sums of the T14 logs.

Round 3 BLOCKERs

  1. CLOSED. A field store borrowing its source while firmware runs. aml-check fieldborrow and fieldborrow string exit 101 with RefCell already borrowed at 488a05a57 and 0 at the head; a_field_store_writes_its_source_as_it_was_when_firmware_changes_it is red under m06 and under the whole revert. By reading, no other borrow of a Data, List or slot is held across a call that can reach firmware's code: write_field's held is of the store's own copy, and invoke_in's is of a table no object names.
  2. CLOSED. An unresolved element's Path outside the meter. fill-lazy-long: 713,799,584 bytes held at 488a05a57, 15,953,360 at the head; m07 and m08 each turn tables_and_names_are_held_against_the_live_bound red. The constant beside each node and the arena a refused load leaves are in the track with numbers, owner and exit.
  3. CLOSED as filed: the secondary bus is checked against the bridge's own, nothing is kept, and the two-bridge test is red under m01 to m05. Reopened in part by what the check leaves unasked: the BLOCKER below.
  4. CLOSED. Name resolution charged no step. The five walks and the six operators each ran 100,000 to 260,000 iterations before the bound at 488a05a57 and about 200 to 4,200 at the head (_OSI 87,381, which now does no work its argument sizes); m09 to m20 each turn its own case of a_walk_or_a_read_a_table_sizes_is_charged_for_all_of_it red and no other. By reading every operator in exec.rs and field.rs again I find no further read or walk a table sizes that goes uncharged.

Evidence at the head: cargo run -- --ci host EXIT=0, Host: 76 step(s), all green, the crate's four suites in it at 27, 22, 18 and 17; the red test result lines in that log are the gate's own controls reaching their verdicts. No program depends on the crate (userland/Cargo.lock alone names it), so no guest test is reached.

Privacy

Passes. The local tables were unpacked into scratch and removed again. Every header OEM ID, OEM table ID and creator ID, every printable string of seven characters or more (3,670), every table length, and the machine-identifying tokens of the capture's boot log were searched for in the diff, the nine commit messages, the title and body and all nine comments: the hits are Microsoft's published _OSI strings, Microsoft Windows NT, and generic words. No table's length, no address and no model, BIOS or serial string is posted. One sentence of the body about names is false of the tree, under NOTE.

Is the refusal right

For the rule itself, yes. A bridge whose Secondary Bus Number is not above its own bus forwards no configuration access, so there is no bus number that reaches the device below it; the only access the interpreter could make instead is to another device's registers, and a store would write them. Refusing is the one answer that touches nothing it was not asked to.

What it costs is not this pull request's to decide and is not yet measurable from it. A table that reads below such a bridge while it loads is refused whole, so on a machine whose firmware leaves that bridge at reset one SSDT's namespace is absent, where before it loaded on a value read from the wrong device. Firmware that does this is, by my memory of §6.5.4 (_REG), outside what the specification lets it assume: only PCI_Config on a root bus that names a _BBN is always accessible. Check that against the text and cite it at the rule if it holds. Whether the T14's firmware leaves that bridge configured is one byte read on the machine, which only the orchestrator can take; if it reads 0, whether a table real firmware ships may be refused for it is the owner's to rule, and none of his three rulings covers it: the third is about opcodes. The track records the fact but gives it no exit that reads it; under NOTE.

BLOCKER

  • userland/acpiserver/aml/src/field.rs:208-218 — offset 0x19 is read as a Secondary Bus Number from every Device between the host bridge and the region's device without asking whether that function is a bridge, or is there at all — the access then lands on a bus nothing named, the defect round 3's third BLOCKER was about, by two other answers.
    • The register exists only in the header layout the Header Type register (offset 0x0E, low seven bits 1) names. A function that is absent, as a root port the firmware disabled and hid is, answers all ones: 0xFF is above every bus, passes answered <= bus, and the endpoint is addressed on bus 255. A Device that is no bridge (ASL nests non-PCI children with an _ADR under controllers, and since this round a region a method declares inside one counts) answers a byte of its third base address register, and any value above the bus is taken.
    • The comment at :212-214 states the invariant, "any other answer would address a device that is not below this bridge", and these two answers break it. By reading; unmeasured.
    • Test to add, each refused with only the bridge's own registers read and nothing accessed below: the upper bridge answering 0x00 at 0x0E and 0x45 at 0x19; and answering 0xFF at both. The passing arm's bridges answer 0x01 at 0x0E. Mutation it must turn red: the Header Type check removed.
    • This widens the refusal to the hidden-port case, which today loads by reading bus 255. Measure what that costs on the T14's tables: the out-of-tree check run a fourth way, every bridge absent (all ones), beside the three recorded, with --bridges answering the Header Type too.

NOTE

  • issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md:116-126 — the bullet that records the bridge refusal names an owner and no exit, and the power-off stage's own exit cannot read it: \_S5 is in the DSDT, so the machine powers off with an SSDT refused. Give it an exit a row can fail: on the T14 every one of the 14 tables' load results is logged and read, and a refusal for a bridge's answer is brought to the owner with the bridge's Header Type and bus registers as the firmware left them.
  • userland/acpiserver/aml/src/field.rs:216 — once §6.5.4 is checked, the refusal's reason at the site is that sentence of the specification, not only the register's reset value.
  • PR body, round 4 NOTEs, and 98ffa8753's message; issues/qemus-interrupt-links-are-refused-by-the-aml-interpreter.md:9-10 — "what the tests and those tables still share and main did not hold is … _BBN, _PR_, and DAT0" is false by my search: five more NameSegs in tests/ occur in the local tables as names and not on main, all generic words, one of them the name my round 3 test sketch used; I do not list them here, and they identify nothing. The issue file still says the host bridge is "the nearest scope that names a _BBN", which is now the nearest Device.

SEND BACK

Japabu and others added 2 commits October 7, 2026 16:28
The walk from a host bridge down to a PCI_Config region's device read
offset 0x19 of every Device between them as a Secondary Bus Number. That
register exists only in header layout 1. A function that is absent, as a
root port the firmware disabled and hid is, answers all ones: 0xFF is above
every bus, so the region's device was addressed on bus 255. A Device that
is no bridge answers a byte of something else, and any value above its bus
was taken.

Each bridge is now asked for its Header Type first (offset 0x0E, the low
seven bits), and a region below a function of any other layout is refused,
Rule, with that one register read and nothing accessed below.

ACPI 6.5 §6.5.4 was read for the rule and is cited at it: OSPM guarantees a
PCI_Config region always accessible only "on a PCI root bus containing a
_BBN object", and one below a bridge is ready "as soon the host controller
or bridge controller has been programmed with a bus number".

The bridge test's passing arm answers layout 1 at both bridges, the upper
with the multi-function bit set; it gains four refusals: layout 0 with a
plausible byte at 0x19, all ones at both, layout 2, and the multi-function
bit alone.

The track's bullet on the T14's tables gains an owner's exit a row can
fail and what the out-of-tree check found with the bridges answered five
ways. The QEMU issue's "nearest scope" is the nearest Device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Round 5's mutations of the bridge rule, at 7d130196a. Each patch is checked, applied, built, its test run and the patch reversed by one script, run.sh, from a clean tree; its log follows the patches whole. m00 is this round's whole source change reverted (git diff 7d130196a 98ffa8753 -- userland/acpiserver/aml/src), the negative control. m01 and m04 are round 4's, cut again for this head; m28 to m30 are round 4's m02, m03 and m05 on the changed lines.

The script:

#!/bin/zsh
# Each mutation: checked, applied, built, its test run, restored. Run from the worktree root, on a clean tree.
S=<scratch>/aml-r5
M=userland/acpiserver/aml/Cargo.toml
T=aarch64-apple-darwin
out=$S/mutations/run.log
BRIDGE=regions:a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus
echo "head $(git rev-parse --short HEAD), $(git status --porcelain --ignore-submodules=none | wc -l | tr -d ' ') path(s) differ; $(uptime)" > $out
one() {
  p=$1; shift
  echo "=== $p" >> $out
  git apply --check $S/mutations/$p.patch || { echo "CHECK FAILED" >> $out; return; }
  git apply $S/mutations/$p.patch
  cargo test --manifest-path $M --target $T --no-run > $S/mutations/$p.build.log 2>&1; echo "build EXIT=$?" >> $out
  for spec in "$@"; do
    bin=${spec%%:*}; name=${spec#*:}
    cargo test --manifest-path $M --target $T --test $bin -- --exact $name > $S/mutations/$p.$name.log 2>&1; echo "$bin $name EXIT=$?" >> $out
    grep -h "panicked at\|left:\|right:\|^0x" $S/mutations/$p.$name.log >> $out
  done
  git apply -R $S/mutations/$p.patch
  echo "restored: $(git status --porcelain --ignore-submodules=none | wc -l | tr -d ' ') path(s) differ" >> $out
}
for p in $S/mutations/m*.patch; do one ${${p:t}%.patch} $BRIDGE; done
echo "final: $(git status --porcelain --ignore-submodules=none | wc -l | tr -d ' ') path(s) differ; $(uptime)" >> $out
echo done > $S/mutations/DONE

m00-the-whole-source-change-reverted.patch:

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..9d1f5f4f8 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -175,16 +175,10 @@ impl Machine<'_> {
     /// the high word, function in the low), in the segment group the host
     /// bridge's `_SEG` names or 0 without one (§6.5.6). The host bridge is
     /// the nearest device naming a `_BBN`, which is the bus directly below it
-    /// (§6.5.5); each device between it and the region's is a PCI-to-PCI
-    /// bridge by its Header Type register, whose Secondary Bus Number
-    /// register is the bus below it (PCI-to-PCI Bridge Architecture
-    /// Specification 1.2, §3.2.5.4). A region declared in the host bridge
-    /// itself addresses the bridge.
-    ///
-    /// §6.5.4 holds a PCI_Config region accessible always only on a root bus
-    /// naming a `_BBN`, and one below a bridge once "the bridge controller
-    /// has been programmed with a bus number": a region below anything else
-    /// is refused, where any bus chosen for it would be another device's.
+    /// (§6.5.5); each device between it and the region's is a bridge, whose
+    /// Secondary Bus Number register is the bus below it (PCI-to-PCI Bridge
+    /// Architecture Specification 1.2, §3.2.5.4). A region declared in the
+    /// host bridge itself addresses the bridge.
     ///
     /// Every access asks again, firmware's methods and the bridges both:
     /// nothing is kept that a bridge renumbered since would make stale.
@@ -213,22 +207,11 @@ impl Machine<'_> {
         let Some((&device, bridges)) = below.split_first() else { return self.function(host, segment, bus) };
         for &bridge in bridges.iter().rev() {
             let b = self.function(bridge, segment, bus)?;
-            let register = |m: &mut Self, offset| {
-                let at = Address::PciConfig { segment, bus, device: b.device, function: b.function, offset };
-                m.host.read(at, crate::Access::Byte).map(|v| v as u8).map_err(|d| Error::Host(d.0))
-            };
-            // Offset 0x19 is a Secondary Bus Number only in header layout 1,
-            // the low seven bits of the Header Type: a function that is
-            // absent answers all ones, and any other layout a byte of
-            // something else.
-            if register(self, 0x0E)? & 0x7F != 0x01 {
-                return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"));
-            }
-            let answered = register(self, 0x19)?;
-            // §6.5.4: the region is ready once its bridge has a bus number.
-            // The register resets to 0, and a bridge's secondary bus is
-            // above the bus the bridge is on: any other answer would address
-            // a device that is not below this bridge.
+            let secondary = Address::PciConfig { segment, bus, device: b.device, function: b.function, offset: 0x19 };
+            let answered = self.host.read(secondary, crate::Access::Byte).map_err(|d| Error::Host(d.0))? as u8;
+            // The register resets to 0, and a configured bridge's secondary
+            // bus is above the bus the bridge is on: any other answer would
+            // address a device that is not below this bridge.
             if answered <= bus {
                 return Err(Error::Rule("a bridge's Secondary Bus Number is not above its own bus, and names no bus below it"));
             }

m01-the-bridges-are-walked-from-the-device-up.patch:

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..98e97b387 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -211,7 +211,7 @@ impl Machine<'_> {
         let mut bus = u8::try_from(bus).map_err(|_| Error::Rule("a _BBN above 0xFF (§6.5.5)"))?;
         let segment = u16::try_from(segment).map_err(|_| Error::Rule("a _SEG above 0xFFFF (§6.5.6)"))?;
         let Some((&device, bridges)) = below.split_first() else { return self.function(host, segment, bus) };
-        for &bridge in bridges.iter().rev() {
+        for &bridge in bridges.iter() {
             let b = self.function(bridge, segment, bus)?;
             let register = |m: &mut Self, offset| {
                 let at = Address::PciConfig { segment, bus, device: b.device, function: b.function, offset };

m04-a-method-counts-as-a-device.patch:

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 9d1f5f4f8..8b0527b95 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -188,7 +188,7 @@ impl Machine<'_> {
         let mut host = r.scope;
         loop {
             self.step()?;
-            if matches!(self.ns.object(host), Some(Object::Device)) {
+            if true {
                 if self.ns.child(host, bbn).is_some() {
                     break;
                 }

m21-the-header-type-is-not-asked.patch:

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..11f6fac9b 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -221,7 +221,7 @@ impl Machine<'_> {
             // the low seven bits of the Header Type: a function that is
             // absent answers all ones, and any other layout a byte of
             // something else.
-            if register(self, 0x0E)? & 0x7F != 0x01 {
+            if false {
                 return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"));
             }
             let answered = register(self, 0x19)?;

m22-the-header-type-is-read-and-not-checked.patch:

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..a5d64b0d4 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -221,7 +221,7 @@ impl Machine<'_> {
             // the low seven bits of the Header Type: a function that is
             // absent answers all ones, and any other layout a byte of
             // something else.
-            if register(self, 0x0E)? & 0x7F != 0x01 {
+            if register(self, 0x0E).is_err() {
                 return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"));
             }
             let answered = register(self, 0x19)?;

m23-the-multi-function-bit-counts-as-layout.patch:

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..34cc45828 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -221,7 +221,7 @@ impl Machine<'_> {
             // the low seven bits of the Header Type: a function that is
             // absent answers all ones, and any other layout a byte of
             // something else.
-            if register(self, 0x0E)? & 0x7F != 0x01 {
+            if register(self, 0x0E)? != 0x01 {
                 return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"));
             }
             let answered = register(self, 0x19)?;

m24-only-layout-zero-is-refused.patch:

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..4f7680606 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -221,7 +221,7 @@ impl Machine<'_> {
             // the low seven bits of the Header Type: a function that is
             // absent answers all ones, and any other layout a byte of
             // something else.
-            if register(self, 0x0E)? & 0x7F != 0x01 {
+            if register(self, 0x0E)? & 0x7F == 0x00 {
                 return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"));
             }
             let answered = register(self, 0x19)?;

m25-only-an-absent-function-is-refused.patch:

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..7ef9dd35a 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -221,7 +221,7 @@ impl Machine<'_> {
             // the low seven bits of the Header Type: a function that is
             // absent answers all ones, and any other layout a byte of
             // something else.
-            if register(self, 0x0E)? & 0x7F != 0x01 {
+            if register(self, 0x0E)? == 0xFF {
                 return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"));
             }
             let answered = register(self, 0x19)?;

m26-a-cardbus-bridge-counts.patch:

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..2a2fdbd96 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -221,7 +221,7 @@ impl Machine<'_> {
             // the low seven bits of the Header Type: a function that is
             // absent answers all ones, and any other layout a byte of
             // something else.
-            if register(self, 0x0E)? & 0x7F != 0x01 {
+            if !matches!(register(self, 0x0E)? & 0x7F, 0x01 | 0x02) {
                 return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"));
             }
             let answered = register(self, 0x19)?;

m27-the-secondary-bus-is-asked-first.patch:

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..ef37e15a7 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -221,10 +221,10 @@ impl Machine<'_> {
             // the low seven bits of the Header Type: a function that is
             // absent answers all ones, and any other layout a byte of
             // something else.
+            let answered = register(self, 0x19)?;
             if register(self, 0x0E)? & 0x7F != 0x01 {
                 return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"));
             }
-            let answered = register(self, 0x19)?;
             // §6.5.4: the region is ready once its bridge has a bus number.
             // The register resets to 0, and a bridge's secondary bus is
             // above the bus the bridge is on: any other answer would address

m28-any-secondary-bus-is-taken.patch:

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..742f6ac07 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -229,7 +229,7 @@ impl Machine<'_> {
             // The register resets to 0, and a bridge's secondary bus is
             // above the bus the bridge is on: any other answer would address
             // a device that is not below this bridge.
-            if answered <= bus {
+            if false {
                 return Err(Error::Rule("a bridge's Secondary Bus Number is not above its own bus, and names no bus below it"));
             }
             bus = answered;

m29-a-secondary-bus-equal-to-the-bridges-own-is-taken.patch:

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..93f1413fe 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -229,7 +229,7 @@ impl Machine<'_> {
             // The register resets to 0, and a bridge's secondary bus is
             // above the bus the bridge is on: any other answer would address
             // a device that is not below this bridge.
-            if answered <= bus {
+            if answered < bus {
                 return Err(Error::Rule("a bridge's Secondary Bus Number is not above its own bus, and names no bus below it"));
             }
             bus = answered;

m30-the-bridge-is-asked-for-its-primary-bus.patch:

diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs
index 304ac1846..52fc1a548 100644
--- a/userland/acpiserver/aml/src/field.rs
+++ b/userland/acpiserver/aml/src/field.rs
@@ -224,7 +224,7 @@ impl Machine<'_> {
             if register(self, 0x0E)? & 0x7F != 0x01 {
                 return Err(Error::Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"));
             }
-            let answered = register(self, 0x19)?;
+            let answered = register(self, 0x18)?;
             // §6.5.4: the region is ready once its bridge has a bus number.
             // The register resets to 0, and a bridge's secondary bus is
             // above the bus the bridge is on: any other answer would address

Its log, run.log:

head 7d130196a, 0 path(s) differ; 16:34  up 8 days,  4:19, 5 users, load averages: 30.67 28.59 29.00
=== m00-the-whole-source-change-reverted
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' (169340898) panicked at acpiserver/aml/tests/regions.rs:226:5:
  left: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 69, device: 0, function: 0, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 71, device: 0, function: 1, offset: 0 }, Byte)]
 right: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 14 }, Byte), Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 69, device: 0, function: 0, offset: 14 }, Byte), Read(PciConfig { segment: 0, bus: 69, device: 0, function: 0, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 71, device: 0, function: 1, offset: 0 }, Byte)]
restored: 0 path(s) differ
=== m01-the-bridges-are-walked-from-the-device-up
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' (169341931) panicked at acpiserver/aml/tests/regions.rs:225:5:
  left: Err(Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"))
 right: Ok(Integer(134))
restored: 0 path(s) differ
=== m04-a-method-counts-as-a-device
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' (169343278) panicked at acpiserver/aml/tests/regions.rs:253:5:
  left: Err(Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"))
 right: Ok(Integer(134))
restored: 0 path(s) differ
=== m21-the-header-type-is-not-asked
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' (169344926) panicked at acpiserver/aml/tests/regions.rs:226:5:
  left: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 69, device: 0, function: 0, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 71, device: 0, function: 1, offset: 0 }, Byte)]
 right: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 14 }, Byte), Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 69, device: 0, function: 0, offset: 14 }, Byte), Read(PciConfig { segment: 0, bus: 69, device: 0, function: 0, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 71, device: 0, function: 1, offset: 0 }, Byte)]
restored: 0 path(s) differ
=== m22-the-header-type-is-read-and-not-checked
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' (169346702) panicked at acpiserver/aml/tests/regions.rs:243:9:
  left: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 14 }, Byte), Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 69, device: 0, function: 0, offset: 14 }, Byte), Read(PciConfig { segment: 0, bus: 69, device: 0, function: 0, offset: 25 }, Byte)]
 right: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 14 }, Byte)]
restored: 0 path(s) differ
=== m23-the-multi-function-bit-counts-as-layout
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' (169348633) panicked at acpiserver/aml/tests/regions.rs:225:5:
  left: Err(Rule("a device above a PCI_Config region's is no PCI-to-PCI bridge by its Header Type, and has no bus below it"))
 right: Ok(Integer(134))
restored: 0 path(s) differ
=== m24-only-layout-zero-is-refused
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' (169349921) panicked at acpiserver/aml/tests/regions.rs:243:9:
  left: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 14 }, Byte), Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 255, device: 0, function: 0, offset: 14 }, Byte)]
 right: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 14 }, Byte)]
restored: 0 path(s) differ
=== m25-only-an-absent-function-is-refused
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' (169351039) panicked at acpiserver/aml/tests/regions.rs:243:9:
  left: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 14 }, Byte), Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 69, device: 0, function: 0, offset: 14 }, Byte), Read(PciConfig { segment: 0, bus: 69, device: 0, function: 0, offset: 25 }, Byte)]
 right: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 14 }, Byte)]
restored: 0 path(s) differ
=== m26-a-cardbus-bridge-counts
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' (169352203) panicked at acpiserver/aml/tests/regions.rs:243:9:
  left: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 14 }, Byte), Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 69, device: 0, function: 0, offset: 14 }, Byte)]
 right: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 14 }, Byte)]
restored: 0 path(s) differ
=== m27-the-secondary-bus-is-asked-first
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' (169353809) panicked at acpiserver/aml/tests/regions.rs:226:5:
  left: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 14 }, Byte), Read(PciConfig { segment: 0, bus: 69, device: 0, function: 0, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 69, device: 0, function: 0, offset: 14 }, Byte), Read(PciConfig { segment: 0, bus: 71, device: 0, function: 1, offset: 0 }, Byte)]
 right: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 14 }, Byte), Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 69, device: 0, function: 0, offset: 14 }, Byte), Read(PciConfig { segment: 0, bus: 69, device: 0, function: 0, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 71, device: 0, function: 1, offset: 0 }, Byte)]
restored: 0 path(s) differ
=== m28-any-secondary-bus-is-taken
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' (169354977) panicked at acpiserver/aml/tests/regions.rs:234:9:
  left: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 14 }, Byte), Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 0, device: 0, function: 0, offset: 14 }, Byte)]
 right: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 14 }, Byte), Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 25 }, Byte)]
restored: 0 path(s) differ
=== m29-a-secondary-bus-equal-to-the-bridges-own-is-taken
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' (169356478) panicked at acpiserver/aml/tests/regions.rs:234:9:
  left: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 14 }, Byte), Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 25 }, Byte), Read(PciConfig { segment: 0, bus: 64, device: 0, function: 0, offset: 14 }, Byte)]
 right: [Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 14 }, Byte), Read(PciConfig { segment: 0, bus: 64, device: 3, function: 1, offset: 25 }, Byte)]
restored: 0 path(s) differ
=== m30-the-bridge-is-asked-for-its-primary-bus
build EXIT=0
regions a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus EXIT=101
thread 'a_pci_config_region_below_bridges_is_on_the_nearest_ones_secondary_bus' (169357600) panicked at acpiserver/aml/tests/regions.rs:225:5:
  left: Err(Rule("a bridge's Secondary Bus Number is not above its own bus, and names no bus below it"))
 right: Ok(Integer(134))
restored: 0 path(s) differ
final: 0 path(s) differ; 16:35  up 8 days,  4:19, 5 users, load averages: 27.73 28.03 28.79

@Japabu

Japabu commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

The out-of-tree check behind the body's round 5 table. It is round 4's check (its source is the comment above that begins "The out-of-tree check behind the body's round 4 numbers") with the changes below, built with cargo build --release twice: against git archive 98ffa8753 of the crate and the three it depends on for the "at 98ffa8753" column, and against the worktree at 7d130196a for the other. It reads tables from a directory, and a listing of PCI functions from a file, both given on the command line and both outside the tree; no byte, address, bus number or name from either is in this comment or in the tree.

Commands, each <cmd> > <log> 2>&1; echo EXIT=$?:

command at 98ffa8753 at 7d130196a
aml-check tables <the T14's tables> 0, the DSDT refused 0, the same
aml-check tables <the T14's tables> --mem=<one address>:2 0, one SSDT refused 0, one SSDT refused
... --mem=<one address>:2 --bridges=reset 0, one SSDT refused 0, one SSDT refused
... --mem=<one address>:2 --bridges 0, all 14 loaded 0, all 14 loaded
... --mem=<one address>:2 --bridges=absent 0, all 14 loaded 0, one SSDT refused
... --mem=<one address>:2 --pci=<the listing> 0, all 14 loaded 0, all 14 loaded

The host was shared: load averages near 28 while these ran.

Changes to round 4's source, as diff:

62,64c62,66
<     /// Whether a bridge answers its Secondary Bus Number register as one
<     /// configured would: the bus after its own.
<     bridges: bool,
---
>     bridges: Bridges,
>     /// Reads of a function a listing does not hold.
>     unlisted: u64,
>     /// Header Type reads a listing answered: a bridge's layout, another, a function it does not hold.
>     headers: [u64; 3],
66a69,113
> /// What the functions the interpreter asks as bridges answer at their Header
> /// Type (0x0E) and Secondary Bus Number (0x19) registers.
> #[derive(Clone, Default)]
> enum Bridges {
>     /// Zero at both, as every other read.
>     #[default]
>     Zero,
>     /// A bridge at reset: layout 1, secondary bus 0.
>     Reset,
>     /// A configured bridge: layout 1, the bus after its own.
>     Configured,
>     /// No function there: all ones at both.
>     Absent,
>     /// As a listing of a machine's functions reads: (bus, device, function)
>     /// to the bytes at 0x0E, 0x18, 0x19 and 0x1A, every other register of a
>     /// listed function zero. A function it does not list answers all ones to
>     /// every read.
>     Listed(BTreeMap<(u8, u8, u8), [u8; 4]>),
> }
> 
> fn all_ones(w: Access) -> u64 {
>     match w {
>         Access::Byte => 0xFF,
>         Access::Word => 0xFFFF,
>         Access::DWord => 0xFFFF_FFFF,
>         Access::QWord => u64::MAX,
>     }
> }
> 
> /// `SSSS:BB:DD.F class=... header_type=0xHH pri/sec/sub= PP SS UU`, a line a function.
> fn listing(path: &str) -> BTreeMap<(u8, u8, u8), [u8; 4]> {
>     let hex = |s: &str| u8::from_str_radix(s.trim_start_matches("0x"), 16).unwrap();
>     std::fs::read_to_string(path)
>         .unwrap()
>         .lines()
>         .map(|l| {
>             let f: Vec<&str> = l.split_whitespace().collect();
>             let at: Vec<&str> = f[0].split([':', '.']).collect();
>             assert!(at[0] == "0000" && f.len() == 7, "a line of another shape");
>             let header = hex(f[2].strip_prefix("header_type=").unwrap());
>             ((hex(at[1]), hex(at[2]), hex(at[3])), [header, hex(f[4]), hex(f[5]), hex(f[6])])
>         })
>         .collect()
> }
> 
84c131,149
<             Address::PciConfig { bus, offset: 0x19, .. } if self.bridges && w == Access::Byte => u64::from(bus) + 1,
---
>             Address::PciConfig { bus, device, function, offset, .. } => match (&self.bridges, offset, w) {
>                 (Bridges::Reset | Bridges::Configured, 0x0E, Access::Byte) => 0x01,
>                 (Bridges::Configured, 0x19, Access::Byte) => u64::from(bus) + 1,
>                 (Bridges::Absent, 0x0E | 0x19, Access::Byte) => 0xFF,
>                 (Bridges::Listed(l), _, _) => match (l.get(&(bus, device, function)), offset, w) {
>                     (None, _, _) => {
>                         self.unlisted += 1;
>                         self.headers[2] += u64::from(offset == 0x0E && w == Access::Byte);
>                         all_ones(w)
>                     }
>                     (Some(r), 0x0E, Access::Byte) => {
>                         self.headers[usize::from(r[0] & 0x7F != 1)] += 1;
>                         u64::from(r[0])
>                     }
>                     (Some(r), 0x18..=0x1A, Access::Byte) => u64::from(r[usize::from(offset) - 0x17]),
>                     (Some(_), _, _) => 0,
>                 },
>                 _ => 0,
>             },
116,117c181,182
<             "reads mem/io/pci/ec {:?} writes {:?} sleeps {} stalls {} notifies {} locks {}",
<             self.reads, self.writes, self.sleeps, self.stalls, self.notifies, self.locks
---
>             "reads mem/io/pci/ec {:?} writes {:?} sleeps {} stalls {} notifies {} locks {} reads of an unlisted function {} listed header types bridge/other/absent {:?}",
>             self.reads, self.writes, self.sleeps, self.stalls, self.notifies, self.locks, self.unlisted, self.headers
293c358
< fn tables(dir: &str, show: bool, mem: &BTreeMap<u64, u64>, bridges: bool) {
---
> fn tables(dir: &str, show: bool, mem: &BTreeMap<u64, u64>, bridges: &Bridges) {
314c379
<         let mut h = Count { mem: mem.clone(), range, bridges, ..Count::default() };
---
>         let mut h = Count { mem: mem.clone(), range, bridges: bridges.clone(), ..Count::default() };
352c417
<     let mut h = Count { mem: mem.clone(), range, bridges, ..Count::default() };
---
>     let mut h = Count { mem: mem.clone(), range, bridges: bridges.clone(), ..Count::default() };
385c450
<     let mut h = Count { mem: mem.clone(), range, bridges, ..Count::default() };
---
>     let mut h = Count { mem: mem.clone(), range, bridges: bridges.clone(), ..Count::default() };
579c644
<             tables(&a[2], show, &mem, a.iter().any(|x| x == "--bridges"))
---
>             tables(&a[2], show, &mem, &bridges(&a))
832a898,909
> 
> fn bridges(a: &[String]) -> Bridges {
>     let mut chosen = a.iter().filter_map(|x| match x.as_str() {
>         "--bridges" => Some(Bridges::Configured),
>         "--bridges=reset" => Some(Bridges::Reset),
>         "--bridges=absent" => Some(Bridges::Absent),
>         x => x.strip_prefix("--pci=").map(|p| Bridges::Listed(listing(p))),
>     });
>     let one = chosen.next().unwrap_or_default();
>     assert!(chosen.next().is_none(), "one answer for the bridges");
>     one
> }

@Japabu

Japabu commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #739 at 7d130196a, round 5, against origin/main (ff57dc704, which is the branch's merge base) and its .claude/agents/reviewer.md. Reviewed since 98ffa8753: the merge ff1d3cff7, which carries no hand edit (its tree differs from 98ffa8753 only by main's files), and 7d130196a.

Net: 17 files, +5893/−3. Production: +3435 (src/). Tests: +2309. Issues, manifests and lock: +149/−3. This round: 4 files, +79/−31; production +26/−9 in field.rs, tests +32/−14, issues +21/−8. The production growth is the Header Type question and its refusal; accepted.

Read, not run. What I measured myself: the privacy search, the diff of the six out-of-tree runs between the two heads, and the cited sentences against the saved chapter of the specification.

Round 4 BLOCKER

CLOSED. Offset 0x19 read from every Device above the region's without asking whether it is a bridge.

  • field.rs:216-236 asks offset 0x0E first and refuses unless its low seven bits are 1; the Secondary Bus Number is read only after that.
  • The test's four refusals (layout 0 with 0x45 at 0x19, all ones at both, layout 2, the multi-function bit alone) each assert that the upper bridge's Header Type is the only access. The passing arm answers 0x81 and 0x01.
  • The mutation I named, m21 (the check replaced by false): build EXIT=0, test EXIT=101 at regions.rs:226. m00, this round's source change reverted under the head's test: EXIT=101. m22 to m30 and the re-run m01 and m04 each EXIT=101, each at an assertion that names its own defect (the read list for order and extra reads, the value for the mask). The tree was clean after each by the script's own log.
  • The cost I asked for, every function absent: at 98ffa8753 all 14 tables loaded and no method was refused, the regions addressed on bus 255; at the head one SSDT and 50 methods are refused. The logs of the two heads differ in nothing else but the Header Type reads and timings.

Round 4 NOTEs

  1. CLOSED. The track's bullet has an owner and an exit a row can fail for a table: all 14 load results logged and read on the machine. What it does not read is under NOTE.
  2. CLOSED. §6.5.4 is cited at the rule and at the test. Both quoted sentences, and the one on a bridge turned off or disabled, are in the saved capture of chapter 6 word for word.
  3. CLOSED. The body's sentence is withdrawn where it stood; the QEMU issue says "nearest Device".

Evidence at the head

cargo run -- --ci host: EXIT=0, Host: 76 step(s), all green, the crate's step in it at 27, 22, 18 and 17, the bridge test among them; the FAILED lines in that log are the gate's controls reaching their verdicts. The crate's own run: EXIT=0. No program depends on the crate, so no guest test is reached. The six out-of-tree runs each end EXIT=0 at both heads and say what the body's table says of them.

The listing run, and whether the refusal is right

The measurement holds as stated. With the bridges answered from a local listing of the real machine's functions under Linux, all 14 tables load at both heads. At 98ffa8753 the sweep made 91 reads of a function the listing does not hold and refused nothing. At the head it asked 51 Header Types: 9 answered a bridge's layout, none another layout, 42 were of a function the listing does not hold, and 42 methods are refused, all by the Header Type rule. No listed function was refused, and no refusal came from the bus-number rule.

Refusing is right. A function that is not there forwards nothing, so there is no bus below it: any bus the interpreter chose would be another device's or nobody's, and the previous head chose 255 from the all-ones answer. The one other answer open to it, all ones for a read and a dropped store without asking the host, is the interpreter inventing a device's answer, a silent default. §6.5.4 puts the firmware outside what it may assume: only PCI_Config on a root bus naming a _BBN is always accessible, and with the bridge "turned off or disabled" the regions of its children "are no longer available". A region in the absent function's own Device is unaffected: it is on the _BBN bus, is asked of the host, and reads all ones there.

What the sweep cannot show is whether any of the 42 is a method the server must run. The sweep calls every method without an argument; a start-up walk that stops at a parent that is not present never reaches a child's method, and a wake or event handler that probes below a port from outside it would. That is the later stage's to find, and the track does not yet make it read: under NOTE.

Privacy

The tree, the eleven commit messages, the title and the two comments posted this round pass. Searched for: every address, class and bus triple of the listing, every table length and their sums, and any hex literal of five digits or more in this round's material. The hits are small decimals coinciding with benchmark figures. The test's bus numbers 0x40, 0x45, 0x47 and 0x3F are none of the listing's, and the harness comment shows the listing's line format with placeholders only. The body fails on one figure: under NOTE.

BLOCKER

None.

NOTE

  • issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md:116-138 — the bullet records the 42 methods and its exit reads table loads alone — a method the server evaluates on the machine and has refused for a bridge's answer reaches nobody by that exit. One clause: the same row reads that no method the server evaluated was refused for a bridge's answer, and one that was goes to the owner with the table refusal.
  • PR body, the section on the real machine's tables, first paragraph — the byte total after "the 13 SSDTs beside it" is the sum of the local tables' lengths — a figure from those tables; delete it. Nothing rests on it.
  • PR body, "The listing" — "methods under a Device the tables declare for a function the listing does not hold" is more than the log shows: it shows each refused walk met an absent function, not where the refused method sits.

LAND AFTER NAMED CHANGES

The exit read table loads alone, so a method the server evaluates on the
machine and has refused for a bridge's answer reached nobody by it. The
same T14 row now reads that none was, and one that was goes to the owner
with the table refusal. Review round 5's NOTE on #739.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu
Japabu marked this pull request as ready for review October 7, 2026 14:54
@Japabu
Japabu enabled auto-merge October 7, 2026 14:54
@Japabu
Japabu added this pull request to the merge queue Oct 7, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 7, 2026
userland/Cargo.toml conflicted: #713 added the member `acpiserver` on the
line this branch added `acpiserver/aml`. Both hold, in order.

The track merged without a conflict and came out with two stages named "the
interpreter": #713's, which the ACPI server runs and which owns the T14's
press issue, and this branch's, the crate and its host exit. The press issue
names its owner as the stage "the interpreter", so they are folded into one
stage carrying both exits. This branch's "inside the server that uses it" is
dropped with the fold: /system/bin/acpiserver now exists and does not link
the crate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu
Japabu enabled auto-merge October 7, 2026 15:39
@Japabu
Japabu added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit e701012 Oct 7, 2026
3 checks passed
@Japabu
Japabu deleted the claude/sleepy-cori-ejictl branch October 7, 2026 16:35
github-merge-queue Bot pushed a commit that referenced this pull request Oct 8, 2026
#739 added userland/acpiserver/aml to userland's member list and its three
path packages to userland's lock, both of which this branch deletes. The
member joins the root list as userland/acpiserver/aml; the root lock gains
toyos-aml, and already held toyos-acpi and toyos-bootmap. Its name and
version pairs are again the union of main's five locks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants