Skip to content

user_ptr: a user copy under its own space's lock is refused by name (S0-a) - #1146

Closed
Japabu wants to merge 1 commit into
mainfrom
s0a
Closed

Japabu wants to merge 1 commit into
mainfrom
s0a

Conversation

@Japabu

@Japabu Japabu commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Memory track S0-a, the finding issues/a-user-copy-demand-pages-under-whatever-its-caller-holds.md: its exit, by a check that fails rather than an argument about callers.

Problem

A user copy reaches its pages under the current space's lock, populating any it hasn't touched yet. A caller that copied while holding that lock spun on itself until the ticket lock's DEADLOCK panic at 500M spins. And it happened only when some page of the window was still unpopulated, so the bug hid wherever the test coverage happened to populate the pages first.

Now

  • The space records which CPU holds its lock. SpaceGuard sets it on acquire and its drop clears it. Holding a sync::Lock disables preemption, so the holder stays on its CPU.
  • Every copy entry refuses a copy made under that lock, by name, whether or not a page needs populating. The entries are View::window, View::pieces and translate_user. The cost is one relaxed load and compare per copy entry.
  • The finding's other three locks need no check. A copy's fault-in no longer takes PROCESS_TABLE, process_data or the VFS lock: handle_page_fault and AddressSpace::reach take the space's lock alone.

Not in this PR

The swap track's S0-a row has a larger half: every user copy outside every kernel lock. The pipe and message copies still run under their locks (issues/a-pipes-lock-spans-a-user-copy-and-its-first-allocation.md). Memory stage 7b needs that half before a fault may park on a pager, and it is the next step.

Records

The orchestrator's ruling (2026-11-01, on the owner's delegation) that 7b comes before stage 4, in the process-memory track.

Checks

Evidence: https://github.com/ToyOSOrg/evidence/tree/da30fd3c533732da42cd43e9160fd95445cb1d8c/r0

  • cargo run -- --ci host: rc=0, 108 steps green.
  • QEMU x86_64, the testcases boot via --debug: 5 members exit 0, and the boot raised no refusal.
  • Guest control G3: sys_paged_read holds the space's lock across its copy. It is refused at once, by name, with a backtrace through PagerObject::read.
  • No host test: the check lives in kernel/src and has no pure model. The guest boot is its coverage.
  • Metal: one testcases boot per bench, staged and not run. Its rule is no refusal on hardware.

🤖 Generated with Claude Code

A user copy reaches its pages under the current space's lock, populating
the ones not yet touched. A caller that copied while holding that lock
spun on itself until the ticket lock's 500M-spin DEADLOCK panic, and
only when a page in the window happened to be unpopulated: a hazard that
depended on coverage.

The space now records which CPU holds its lock (SpaceGuard sets it, its
drop clears it; holding a sync::Lock keeps the holder on its CPU).
Every copy entry, View::window, View::pieces and translate_user, refuses
a copy this CPU makes under that lock with a named panic, whether or not
any page needs populating. The check is one relaxed load and compare per
copy entry.

The finding's other locks, PROCESS_TABLE, process_data and the VFS lock,
are no longer taken by a copy's fault-in: handle_page_fault and
AddressSpace::reach take the space's lock alone. So that order is
refused by the check and no argument about callers is needed.

The swap track's S0-a row keeps its larger half: every user copy outside
every kernel lock (the pipe and message copies), which memory stage 7b
needs before a fault may park on a pager.

Records: the orchestrator's ruling of 2026-11-01 (on the owner's
delegation) that memory stage 7b comes before stage 4, in the
process-memory track.

Closes issues/a-user-copy-demand-pages-under-whatever-its-caller-holds.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 2, 2026
…fused (S0-a)

A user copy reaches its pages under its space's lock, populating any not
yet touched, and in memory stage 7b that fault-in will park on a userland
pager. A copy made under any spin lock would then park holding it. Several
syscalls copied that way: reads and writes under the process lock and the
object's (a pipe's, a kernel file's cache, a console's line, a device
claim's description), message send and receive under the process and queue
locks, and getcwd, query_modules, sysinfo and readlink under the process,
process-table or VFS lock.

The gate. Every copy entry (View::window, View::pieces, translate_user)
refuses a copy above the preemption depth its syscall's entry runs at,
whether or not a page needs populating: every spin lock raises the count,
and both architectures' trap entries raise it to BASELINE_TRAP (x86_64's
syscall_entry and common_entry, AArch64's trap::syscall and
trap::user_abort). It is a kernel-bug fence; no user action reaches it.
This replaces #1146's per-space holder, which caught only a copy under its
own space's lock.

The moves.
- Syscalls work on their object cloned out of the process's lock
  (syscall::handles::object, and with_object now clones). A device claim is
  read under it, since describing one installs handles, into a DeviceRead
  copied out after.
- getcwd clones the cwd, readlink lets the VFS lock go first, and
  query_modules and sysinfo lay their answer out under their lock and copy
  it out after.
- A kernel file's position and a console's line are sleeping locks
  (SleepLock), which raise nothing, so a read or a write stays atomic with
  its position update (XSH 2.9.7) and a console write keeps its lines whole.
  file_cache reads copy from the page's Arc with the cache's lock let go (a
  write meanwhile writes a copy), and a write's bytes are copied in a page at
  a time before the cache's lock. ByteSource had no other user and goes.
- Pipes and connections claim their bytes (kernel::pipe::Bytes): one write
  claim and one read claim at a time, each copying with no lock held. The
  lock claims, hands out a chunk's frames and ends the claim. A claimed
  write's frames count as used, so no surplus takes them, and only the read's
  own consume lets claimed bytes go; a last reader leaving under a claim
  leaves the frames to the claim's end. A second copy of either kind waits
  for the first on its end's watch under a TURN token, cancellable by a kill
  and waking no poll. Frames and a longer frame list are taken before the
  lock and spares given back after it (frame_bytes::supply, give_back), so
  neither the allocator nor the heap is reached under it.
- A receive claims the front message, copies its bytes out, then under the
  process and queue locks pops it and installs its handles, with the table's
  room made first. A claim dropped first leaves the message queued whole, so
  a refusal still takes nothing. A send claims its room, copies in, then
  moves its handles and queues the message whole in one hold; a receiver
  gone by then hands the batch back at its own numbers.

The console's partial line is now flushed in Drop, at the last reference:
the deferred release queue runs on_zero_handles where no sleeping lock is
taken, and a write in flight holds a reference.

Records: the finding a-user-copy-demand-pages-under-whatever-its-caller-holds
is closed (its order is refused by a check that fails). The pipe issue keeps
what is left: the hold time unread on a bench, the queue's two lists grown
under its lock, and audio_tone's worst wake. The process-memory track records
the orchestrator's ruling of 2026-11-01 (on the owner's delegation) that 7b
comes before stage 4, its S0-a prerequisite met, and that a copy under a
sleeping lock parks holding it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 2, 2026
…fused (S0-a)

A user copy reaches its pages under its space's lock, populating any not
yet touched, and in memory stage 7b that fault-in will park on a userland
pager. A copy made under any spin lock would then park holding it. Several
syscalls copied that way: reads and writes under the process lock and the
object's (a pipe's, a kernel file's cache, a console's line, a device
claim's description), message send and receive under the process and queue
locks, and getcwd, query_modules, sysinfo and readlink under the process,
process-table or VFS lock.

The gate. Every copy entry (View::window, View::pieces, translate_user)
refuses a copy above the preemption depth its syscall's entry runs at,
whether or not a page needs populating: every spin lock raises the count,
and both architectures' trap entries raise it to BASELINE_TRAP (x86_64's
syscall_entry and common_entry, AArch64's trap::syscall and
trap::user_abort). It is a kernel-bug fence; no user action reaches it.
This replaces #1146's per-space holder, which caught only a copy under its
own space's lock.

The moves.
- Syscalls work on their object cloned out of the process's lock
  (syscall::handles::object, and with_object now clones). A device claim is
  read under it, since describing one installs handles, into a DeviceRead
  copied out after.
- getcwd clones the cwd, readlink lets the VFS lock go first, and
  query_modules and sysinfo lay their answer out under their lock and copy
  it out after.
- A kernel file's position and a console's line are sleeping locks
  (SleepLock), which raise nothing, so a read or a write stays atomic with
  its position update (XSH 2.9.7) and a console write keeps its lines whole.
  file_cache reads copy from the page's Arc with the cache's lock let go (a
  write meanwhile writes a copy), and a write's bytes are copied in a page at
  a time before the cache's lock. ByteSource had no other user and goes.
- Pipes and connections claim their bytes (kernel::pipe::Bytes): one write
  claim and one read claim at a time, each copying with no lock held. The
  lock claims, hands out a chunk's frames and ends the claim. A claimed
  write's frames count as used, so no surplus takes them, and only the read's
  own consume lets claimed bytes go; a last reader leaving under a claim
  leaves the frames to the claim's end. A second copy of either kind waits
  for the first on its end's watch under a TURN token, cancellable by a kill
  and waking no poll. Frames and a longer frame list are taken before the
  lock and spares given back after it (frame_bytes::supply, give_back), so
  neither the allocator nor the heap is reached under it.
- A receive claims the front message, copies its bytes out, then under the
  process and queue locks pops it and installs its handles, with the table's
  room made first. A claim dropped first leaves the message queued whole, so
  a refusal still takes nothing. A send claims its room, copies in, then
  moves its handles and queues the message whole in one hold; a receiver
  gone by then hands the batch back at its own numbers.

The console's partial line is now flushed in Drop, at the last reference:
the deferred release queue runs on_zero_handles where no sleeping lock is
taken, and a write in flight holds a reference.

Records: the finding a-user-copy-demand-pages-under-whatever-its-caller-holds
is closed (its order is refused by a check that fails). The pipe issue keeps
what is left: the hold time unread on a bench, the queue's two lists grown
under its lock, and audio_tone's worst wake. The process-memory track records
the orchestrator's ruling of 2026-11-01 (on the owner's delegation) that 7b
comes before stage 4, its S0-a prerequisite met, and that a copy under a
sleeping lock parks holding it.

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

Japabu commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

Folded into #1150, which keeps this PR's gate (corrected to refuse only above the syscall entry's preemption depth) and drops the per-space holder, by the orchestrator's ruling. Its metal boots are superseded by #1150's.

@Japabu Japabu closed this Nov 2, 2026
Japabu added a commit that referenced this pull request Nov 3, 2026
…fused (S0-a)

A user copy reaches its pages under its space's lock, populating any not
yet touched, and in memory stage 7b that fault-in will park on a userland
pager. A copy made under any spin lock would then park holding it. Several
syscalls copied that way: reads and writes under the process lock and the
object's (a pipe's, a kernel file's cache, a console's line, a device
claim's description), message send and receive under the process and queue
locks, and getcwd, query_modules, sysinfo and readlink under the process,
process-table or VFS lock.

The gate. Every copy entry (View::window, View::pieces, translate_user)
refuses a copy above the preemption depth its syscall's entry runs at,
whether or not a page needs populating: every spin lock raises the count,
and both architectures' trap entries raise it to BASELINE_TRAP (x86_64's
syscall_entry and common_entry, AArch64's trap::syscall and
trap::user_abort). It is a kernel-bug fence; no user action reaches it.
This replaces #1146's per-space holder, which caught only a copy under its
own space's lock.

The moves.
- Syscalls work on their object cloned out of the process's lock
  (syscall::handles::object, and with_object now clones). A device claim is
  read under it, since describing one installs handles, into a DeviceRead
  copied out after.
- getcwd clones the cwd, readlink lets the VFS lock go first, and
  query_modules and sysinfo lay their answer out under their lock and copy
  it out after.
- A kernel file's position and a console's line are sleeping locks
  (SleepLock), which raise nothing, so a read or a write stays atomic with
  its position update (XSH 2.9.7) and a console write keeps its lines whole.
  file_cache reads copy from the page's Arc with the cache's lock let go (a
  write meanwhile writes a copy), and a write's bytes are copied in a page at
  a time before the cache's lock. ByteSource had no other user and goes.
- Pipes and connections claim their bytes (kernel::pipe::Bytes): one write
  claim and one read claim at a time, each copying with no lock held. The
  lock claims, hands out a chunk's frames and ends the claim. A claimed
  write's frames count as used, so no surplus takes them, and only the read's
  own consume lets claimed bytes go; a last reader leaving under a claim
  leaves the frames to the claim's end. A second copy of either kind waits
  for the first on its end's watch under a TURN token, cancellable by a kill
  and waking no poll. Frames and a longer frame list are taken before the
  lock and spares given back after it (frame_bytes::supply, give_back), so
  neither the allocator nor the heap is reached under it.
- A receive claims the front message, copies its bytes out, then under the
  process and queue locks pops it and installs its handles, with the table's
  room made first. A claim dropped first leaves the message queued whole, so
  a refusal still takes nothing. A send claims its room, copies in, then
  moves its handles and queues the message whole in one hold; a receiver
  gone by then hands the batch back at its own numbers.

The console's partial line is now flushed in Drop, at the last reference:
the deferred release queue runs on_zero_handles where no sleeping lock is
taken, and a write in flight holds a reference.

Records: the finding a-user-copy-demand-pages-under-whatever-its-caller-holds
is closed (its order is refused by a check that fails). The pipe issue keeps
what is left: the hold time unread on a bench, the queue's two lists grown
under its lock, and audio_tone's worst wake. The process-memory track records
the orchestrator's ruling of 2026-11-01 (on the owner's delegation) that 7b
comes before stage 4, its S0-a prerequisite met, and that a copy under a
sleeping lock parks holding it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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