Skip to content

tests: a syscall naming a claim's BAR copies through it, as the kernel's copy contract says - #1199

Merged
Japabu merged 3 commits into
mainfrom
barcopy-refuse-device-copy
Nov 10, 2026
Merged

Japabu merged 3 commits into
mainfrom
barcopy-refuse-device-copy

Conversation

@Japabu

@Japabu Japabu commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

aarch64's virt_claim_lpi has been red on main since #1150: its claim_bar_fault job reads a pipe into its claim's mapped BAR and expected BadAddress, and the kernel answers Ok(16) (issues/a-read-into-a-claims-bar-is-not-refused-on-aarch64.md, run 42124293522 at 0b2dec6).

Cause

Not a kernel regression. #1150 decided that a user copy goes through the process's own mapping and that a window over device memory the process mapped is copied through, making only the accesses the process itself could. It says so in its approved design note ("A user aperture (device memory) is copied through rather than refused") and at kernel/src/user_ptr.rs and both arch::uaccess headers. claim_bar_fault was not updated and kept asserting the contract before it.

Decision (the orchestrator's ruling)

The contract stands: every syscall buffer address is the caller's own in that call, the copy holds no lock, and the kernel survives any fault in it.

Checked before proceeding, by reading the code: no syscall copies to or from a user address the caller did not pass in that call.

  • The copy routines are reached only from user_ptr's private View.
  • A View is made only through the SyscallContext created once per syscall in dispatch.rs, and its windows carry that call's lifetime; no struct keeps one beyond it.
  • The user addresses the kernel does keep are the futex word (read through the direct map, which reaches no BAR), an inbox's rings (the kernel's own object at an address the kernel chose, written through the object) and a child's maps and stack (a new space).
  • A read that parks copies later, at another party's timing, but into the buffer of that same call.

Change

No kernel code changes.

  • claim_bar_fault (aarch64, virt_claim_lpi) asserts the stated contract on edu's two DMA address registers: an aligned 16-byte read leaves the pipe's bytes in them; an aligned write carries them back; an unaligned 8-byte copy either way answers BadAddress with the registers unchanged (AArch64 Device memory); a futex wait and wake there are still refused. The crash-report markers go in after the copies.
  • x86-64 had no row for a copy through a BAR. bar_map_again, which already maps the virtio NIC's common configuration on the suite's boot, carries it in about thirty lines, and that job runs under two rows, bar_map_again and bar_map_behind_a_bridge: a pipe write from device_feature equals the holder's own byte loads, and a pipe read into device_feature_select reaches the register, which is then put back. It needs the guest because it is rep movsb on a real function's registers.
  • The manual's devices page states the rule in one line.
  • The record is deleted as a stale test, with no severity; its durable rule is already at user_ptr's header.

Checks

Evidence, round 2: https://github.com/ToyOSOrg/evidence/tree/ce2a88c6a5ffbbc1ebd3ad29481992809cb142a4/r2

Evidence, round 1: https://github.com/ToyOSOrg/evidence/tree/04c9b03533dff96d3dfba5b5737a94f022ee9717/r1

check revision result
guest-rows virt_claim_lpi (aarch64 virt, emulated), run 42907842461 1710441 PASS
guest-rows bar_map_again (x86-64 q35), run 42907842461 1710441 FAIL, this branch's own defect: the new check wrote zero to device_feature_select where firmware had left one, so the next ask read another feature word. Fixed in f33b58e.
guest-rows bar_map_again, run 42909199077 f33b58e PASS
guest-rows bar_map_again and bar_map_behind_a_bridge (x86-64 q35, the NIC on the root bus and behind a root port), run 42914728075 e56cd25 both PASS
cargo run -- --ci host on macOS 1710441's tree exit 1: 1 of 110 steps red, llvm::tests::the_caller_s_environment_reaches_neither_the_build_nor_the_key, already recorded in issues/the-llvm-environment-test-breaks-git-on-macos.md and independent of this change, which touches no build code.

virt_claim_lpi was not rerun after 1710441: the later commits change bar_map_again.rs, a comment in tests/toyos.rs and the manual's line, none of which its image or judgement carries. No host step builds the guest suite's binaries, and the later tests/toyos.rs edit is a doc comment, so the host result also holds for the head.

Main's red run 42124293522 shows the old expectation was wrong for this kernel. That the new assertions can fail rests on inspection: a kernel on the old contract fails the first Ok(16), one that stores through an unaligned access fails BadAddress or the register comparison, and bar_map_again asserts the register's low byte is not zero before comparing.

Risks

  • The unaligned refusal is shown only under emulation; no AArch64 metal bench exists.
  • bar_map_again's first failure showed that the row's register comparison rests on device_feature_select being whatever firmware left (one, on the runner). That was so before this PR; the new check preserves it.

🤖 Generated with Claude Code

Japabu and others added 2 commits November 10, 2026 23:43
…l's contract says

#1150 made every user copy go through the process's own mapping and
stated, at `user_ptr` and both `arch::uaccess`, that a window over device
memory the process mapped is copied through, making only the accesses
the process could make itself. `claim_bar_fault` still asserted the
contract before it, a `BadAddress` for any buffer in a BAR, and has been
red on aarch64's `virt_claim_lpi` since (run 42124293522 at 0b2dec6:
the read answered `Ok(16)`). The owner ruled the contract stands: the
buffer's address is the caller's own in that call, the copy holds no
lock, and any fault in it is survived.

`claim_bar_fault` now asserts that contract on AArch64: an aligned read
into edu's two DMA address registers leaves the pipe's bytes in them, an
aligned write from them carries those bytes, an unaligned copy either
way answers `BadAddress` with the registers unchanged, and a futex word
there is still refused. The markers the crash reports must not print go
in after the copies.

x86-64 had no row for it. `bar_map_again`, which already holds a mapped
BAR of the virtio NIC on the suite's boot, carries it: a pipe write from
`device_feature` carries what the holder's own byte loads read, and a
pipe read into `device_feature_select` reaches the register.

The manual's devices page states the rule in one line. The record
`a-read-into-a-claims-bar-is-not-refused-on-aarch64` closes as a stale
test; its durable rule is already at `user_ptr`'s header.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Run 42907842461 at 1710441: the copies passed, and the next ask read
another `device_feature` word, because the check wrote zero to
`device_feature_select` where firmware had left one. It now reads the
byte first, has the kernel store the other value, and restores what was
there.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Review r1 of #1199 at f33b58ef2 (base f9b5d7f12)

Architecture

The branch changes no kernel code and no decision. It brings one stale test to the contract #1150's approved design note already states ("A user aperture (device memory) is copied through rather than refused. On AArch64, an unaligned access to Device memory faults to BadAddress"), adds the x86-64 counterpart, and gives the manual its line. That is the right shape: the kernel's headers already carry the rule, so the record's durable line has a home and the file can go.

Putting the x86-64 check inside bar_map_again is acceptable. It needs a mapped BAR of a function no program holds, which that job already has, and a job of its own would cost a binary and a registration for one pipe round trip.

Size: production 0. Tests +103/−28 (bar_map_again.rs +48/−1, claim_bar_fault.rs +48/−22, tests/toyos.rs +7/−5). Manual +2. Record −27.

What I checked

The gap claim holds, by inspection. No syscall copies to or from a user address the caller did not pass in that call.

  • arch::uaccess's three routines are called only from user_ptr's private View (kernel/src/user_ptr.rs:191-210); the only other references are the two fault paths' fixup.
  • SyscallContext::new has one caller, kernel/src/syscall/dispatch.rs:76. No window is stored: the only structs holding one are Incoming/Outgoing (built in dispatch, passed by reference) and log::user::UserRecords (a borrow). There is no 'static window and no generic holder.
  • SMAP and PAN are required, so no other kernel code can touch a user address in place; anything else goes through the direct map.
  • Addresses the kernel keeps: the futex word goes through Space::reach, whose copyable filter refuses an aperture (kernel/src/mm/space.rs:770,878), and the job asserts that on AArch64. Inbox completions are written through the kernel's own object; submissions carry a token and a handle, no buffer address (Nop, Watch, Accept). Space::fill writes only committed private memory.
  • A parked read or receive copies after it wakes, in the caller's own thread, into that call's buffer (kernel/src/pipe.rs:222, kernel/src/syscall/ipc.rs:584-616).
  • The ABI has no syscall that registers a buffer, a completion address or a clear-on-exit word.

One caveat on the wording, not on the fact: SyscallContext<'a>'s lifetime is free (unsafe fn new), so "its windows carry that call's lifetime" rests on inspection, not on the type.

claim_bar_fault asserts the contract and can fail.

  • at is page base + 0x80, so the 16-byte copies are two 8-byte STTR/LDTR, the size edu's DMA registers take. A pipe read or write of that size is one bounce copy at offset 0 (pipe.rs:222, frame_bytes.rs:213), so no split changes the access width.
  • The unaligned window is 8 bytes at at + 1, which is one 8-byte access. user_copy_abort sends any EL1 data abort in a copy body with a valid user FAR to the fixup (kernel/src/arch/aarch64/trap.rs:147-168), so the alignment fault becomes BadAddress. QEMU 11.1.3 on the runner models that fault.
  • 0xa5 bytes differ from the copied ones, so copied("the refused unaligned copies") would catch a store that went through.
  • No separate control is needed: a kernel on the old contract fails the first Ok(16), one that splits an unaligned access fails BadAddress, and one that answers a count without reaching the device fails copied. The rows green before kernel: user copies go through the process's own mapping, and none runs under a lock (S0-a's lock half) #1150 show that the kernel could answer the other way.
  • The crash markers go in after the copies and before the futex and the children, so the harness's console check is as it was.

bar_map_again's addition can fail and leaves the NIC as found.

  • The comparison is with the holder's own byte loads, which is the contract: rep movsb makes byte accesses, and the common configuration answers a byte load, not a slice of the dword.
  • other = before ^ 1 always differs from before, so selected == other shows the kernel's store reached the register. The first version (1 written where firmware had left 1) did not show that; the fix does.
  • The kernel stores exactly one byte (one bounce copy), the job restores before before it judges, and the extra mapping and both pipe handles are gone before the handle-table fill.

Evidence.

  • Runs 42907842461 (171044157, failure) and 42909199077 (f33b58ef2, success) are what the PR says, on the guest runner, QEMU 11.1.3, x86-64 under KVM and aarch64 emulated.
  • virt_claim_lpi need not rerun at the head: f33b58ef2 changes only bar_map_again.rs, which virtclaimcase.toml does not carry.
  • The host red is independent. The log shows SDKROOT=/no/such.sdk breaking xcrun's git inside keyed_and_built, exactly as issues/the-llvm-environment-test-breaks-git-on-macos.md records, and this branch touches nothing under build/. The other 383 tests of that step ran. The host log names no guest binary, so the head's one-file delta does not reach it.

Records. The deleted record has no citation left in the tree (slug and title searched at the head). Its exit's second arm is what this branch does.

Manual. docs/src/architecture/devices.md:26-27 is accurate on both architectures and names no code.

BLOCKER

  1. tests/toyos.rs:6377-6384 — the changed binary runs in a second row that was not run. bar_map_behind_a_bridge boots Profile::HeadlessNicBehindPort, runs test_rs_bar_map_again and requires exit 0. The branch has no run of it at either commit (the branch's dispatches are 42907842461 and 42909199077 only), and the PR does not mention it. This is the same job whose new check already failed once on an assumption about what device_feature_select held on one boot. By inspection the fixed check is agnostic to that value, so I expect green, but an unrun row is how main got the red this PR removes. Cheapest sufficient check: dispatch bar_map_behind_a_bridge at f33b58ef2 and add it to the table. No code change is asked for.

SUGGESTION

  • docs/src/architecture/devices.md:26 — "The kernel copies through a device mapping its process holds" has no subject for the copy and no antecedent for "its". Something like "A syscall buffer inside a device mapping the calling process holds is copied through that mapping, with only the accesses the process itself could make."
  • tests/toyos.rs:6301-6302 — "and no host build's" is hard to parse; "which no host test exercises" says it.
  • tests/guest/rust-tests/src/bin/bar_map_again.rs:80-82 — the first SAFETY says "read and written volatile" over a statement that only reads, and the next says "as above". One comment over the pair, or each saying its own, is clearer.
  • tests/guest/rust-tests/src/bin/bar_map_again.rs:38-39 — "what firmware left there" is an inference from the first run; "whatever the claim found there" is what the code relies on.
  • tests/guest/rust-tests/src/bin/bar_map_again.rs:69-70 — copied == own is vacuous against a kernel that copied zeros only if own[0] is zero. It is not on this device (VERSION_1 or MAC), but assert_ne!(own[0], 0) would make the check say so.

For the orchestrator (PR description and commit message)

  • The body's heading "Decision (the owner's)" and commit 171044157's "The owner ruled the contract stands" attribute the ruling to the owner. My brief says the orchestrator ruled it. If the owner did not rule it himself, both should say whose ruling it is.
  • The body says bar_map_again carries the x86-64 check; it should also say the job runs under bar_map_behind_a_bridge, with that row's result.
  • "Control: … main's red run 42124293522" shows the old expectation was wrong for this kernel. It is not a control that the new assertions can fail; inspection establishes that, as above.

SEND BACK

…eck refuses a zero register

The manual's line says what is copied and for whom. `bar_map_again`'s
comparison with the holder's own byte loads asserts the low byte is not
zero, so a copy of zeros cannot pass it; its comments each say their own
access, and the select is "whatever the claim found there".

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator verification of the fix (f33b58e..e56cd25):

Landing.

@Japabu
Japabu marked this pull request as ready for review November 10, 2026 22:33
@Japabu
Japabu added this pull request to the merge queue Nov 10, 2026
Merged via the queue into main with commit 743b951 Nov 10, 2026
11 checks passed
@Japabu
Japabu deleted the barcopy-refuse-device-copy branch November 10, 2026 22:49
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.

1 participant