Repository navigation
Consolidate storage around native bcachefs and userland filesystems - #1152
Conversation
As persist.write_4k_fsync, each iteration first writing 1 MiB over another 1 MiB file and leaving it unsynced: an fsync of one file that also makes another file's write durable pays for it here. It is the A/B base for a per-file fsync (issues/an-fsync-of-a-served-file-syncs-its-whole-volume.md), which asks for one file's fsync measured beside the volume's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A holder's SYS_PAGED_READ copied a pinned chunk of 16 pages at a time while the file server's write landed page by page in the same frames, so a read beside a write of several pages could return pages from before and after it, or a page torn at the copy (memory stage 7a's recorded gap). Every write of an object's now runs as a write span on it (kernel::paged::Object): each write call and each dirty supply begins one before its first byte lands and ends it after its last, and a resize moves the object's version. A holder's read begins only while no span is open and counts its bytes only where the version has not moved since; a read a write met answers WouldBlock, and the SDK, which already asks the server whenever a holder read fails, reads through the server, whose loop orders the read against the write. A server write takes many calls when a page is absent, in part or at the dirty ceiling. A call that does not take all it was given leaves the span open (held) until the new PagedOp::EndWrite, and later calls nest in it, so the file server's whole write is one span. The server's first call of a write is always a write: one that takes everything is the whole write at no extra syscall; otherwise the server ends the held span on every path, the refused one included. Holder reads never wait on a write: the pager is never made to wait on its readers, and a read that met one costs one round trip. Tests: - kernel::paged: a read counts only where no write began or was under way; a short call holds the write until its end. - fileserver pages: a write of many calls (an absent page supplied, the dirty ceiling written back, a last page in part) is read whole or not at all at every call boundary, and ends whether it lands or is refused. - guest a_served_files_read_never_tears_against_its_write: a writer puts 400 whole generations of a 64-page served file while a reader on its own description reads it whole through its holder; red on a torn read. Closes issues/a-read-and-a-write-of-a-paged-file-can-tear-across-pages.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FSYNC synced the whole volume: every dirty page of every file written back and every open file's entry written before the format's commit. An fsync now makes durable the file it names and only what the volume's commit cannot leave out: - Volume::carried names the open files the next commit carries whatever it is asked: on DATA one whose entry was written since the last commit (a rename, a shrink) or whose reserve took blocks since, which the commit's allocator holds as taken; on FAT every changed open file, since a cache flush writes every chain the cache holds. - Files::fsync writes back the named file's pages and those of every carried file, then Volume::commit writes the named entry and each carried one and commits. A carried file whose pages are refused refuses the commit: its entry would name blocks without its bytes. - So after a stop no entry names a block holding an earlier tenant's bytes. A write over blocks a commit already named is carried by nothing, and waits for its own fsync or the volume's sync. A directory's fsync commits the carried files only: DATA writes every name change into the tree at once. SYNC and the write-back timer still sync the whole volume, and a per-file fsync leaves the timer due. Found while doing this: a sync commits the entry of a file whose pages were refused (issues/a-sync-commits-the-entry-of-a-file-whose-pages-were-refused.md). Tests (fileserver pages): an fsync makes its file durable across a stop and leaves another file's overwrite unwritten; a carried file's pages and entry go with another's fsync, so after a stop on a volume of stale blocks it reads its own bytes; a directory's fsync makes a new name durable. issues/an-fsync-of-a-served-file-syncs-its-whole-volume.md stays open until its measurement, osbench's persist.write_4k_fsync_beside_1m base against head on metal, is recorded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aims narrowed - R1: the holder's answer WouldBlock is documented on paged_read, the boundary a holder implements against; PagedOp::Write keeps only the EndWrite rule. - R3: a carried file's test no longer claims no block is held that no entry names: DataVolume::commit commits past a refused carried entry, whose taken blocks then leak after a stop. Filed with S3, one carried file's refused write-back refusing every fsync on the volume (issues/a-refused-carried-file-costs-every-other-files-fsync-or-leaks-its-blocks.md). - S2: the tear member's generations are its bytes mod 256. R2 is in the fsync commit: its issue stays open until the A/B records the exit's measurement. S1 (the version move at a write's end is redundant) is declined for this round: it changes the kernel the staged A/B images were built from. Comments, ABI docs and issue records only: no code changes, so the staged images at b4edede still measure this head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The volume-wide sync an fsync used to run cleared the write-back the server's loop keeps due. The per-file fsync never did: after it, the server waited every turn with a deadline, which every park and idle entry programs into the CPU's timer, and 2 s later walked the volume in a sync with nothing left to write. The volume now says whether an open file's entry is changed past what its last commit made durable (Volume::owes), and Files adds its dirty pages (Files::owes). An fsync that leaves neither answers the write-back as a sync does. One that leaves another file's write keeps it due from that write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A served file's fstat was a file server round trip. A paged object now
publishes what a holder's fstat needs that can change: the kernel's own
size and write-span version, and the mtime its pager sets.
SYS_PAGED_STAT(holder) answers {size, mtime, version} under the
object's lock, WouldBlock while a write is under way (atomic as a read
is), and the SDK then asks the server. Kind, node, volume and nlink come
from the open's reply, which no change to the file moves.
The owner sets the mtime as a word it already sends: PagedArgs.time on
Write and Resize, paged_create's mtime, and PagedOp::Stamp for a utimens.
The file server hands the object the volume's own value after each
change (reserve, truncate, an open that truncates, a set time, a refused
write's cut back), at the volume's resolution. The version is the
object's data generation; there is no second counter.
FAT's mtime was its entry's last level, not its last write
(issues/a-fat-files-mtime-through-its-handle-is-its-last-level.md),
so it could not be published. A write or resize now records its even
second, which the handle answers until the level stamps it.
Tests:
- kernel::paged: a stat publishes no version while a write is held.
- fileserver pages: the object's size and mtime are the volume's after
an open, a write, a refused write, a truncate and a stamp.
- fileserver fat: a file's mtime is its last write's through its handle,
off the disk a sync later and after a reopen (replaces the test that
pinned the stamp to the flush).
- guest a_held_files_fstat_is_its_servers_stat: the held fstat agrees
with the server's stat of the path after each change.
Closes issues/a-fat-files-mtime-through-its-handle-is-its-last-level.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The orchestrator's ruling on the stage-3 split. The client name cache comes on mapped paged objects, with file slots, since stat by path of an existing file is the row that matters. Built before mappings, its slots would be read by syscall and it would need a names-only pager for SYSTEM and a dirty allowance for BOOT: an interim mechanism the mappings would replace. 3a, a held descriptor's fstat, is this PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preserve the published #1141 and #1144 commits as dependencies. Resolve the syscall-number collision with firmware-window by assigning PAGED_STAT 165 and extending the profile to include both calls. These dependencies remain under their existing review; this merge does not claim approval or land them on main.
Save in-progress implementation before integrating paged-object fixes.
Preserve write-span publication and mtime semantics for anonymous objects.
Remove the kernel pathname and file-offset syscall declarations and SDK fallback. Keep generic object reads, writes and metadata queries. File metadata comes from the server so unlink and link counts stay authoritative; paged reads retain their atomic offset publication. Share runtime cwd through the descriptor owner's SDK ABI, initialized only once from spawn metadata. Derive synthetic read-only root and ancestor directories from the names actually held by the immutable namespace, exposed by a bounded generic namespace inspection operation. Host validation: SDK fd-table check and clippy, 5 files tests, 49 ABI tests, and all 106 libc module tests pass. Combined guest compilation follows the kernel filesystem removal and shared Rust reexport update.
Serve /tmp and diskless DATA roots through the fileserver namespace with one retained anonymous paged object per regular inode. Keep names and open descriptions independent, release unlinked bytes only after the last description and holder, and read sparse holes without allocating frames. Delete kernel VFS, tmpfs, pathname syscalls, file cache and file objects. Executable loading now takes its supplied immutable SharedImage directly. Keep bootstrap cwd transport; SDK routing removal is coordinated separately. Add owner-only advisory pager quota headroom, explicit memory grants, and host plus guest coverage for retention, sparse reads and quota refusal.
Consolidation checkpoint: routes DATA through the on-disk engine and removes the provisional format and its synchronous adapter. Holds inode identities and per-page reservation tokens. Engine token APIs and userland memory changes are integrated in following commits; this checkpoint is not a validated landing. The partition signature test passes with actual bcachefs recognition.
Resolve the volume contract in terms of inode identity and adapt memory reservations to explicit old content bounds. Validation follows combined SDK and engine integration.
Includes generic namespace name inspection, explicit namespace ancestors and authoritative file-server metadata. Preserves existing paged I/O improvements.
Write checksummed or authenticated file blocks through the existing journal and bucket allocator, retaining checksum slices of overwritten extents. Reserve concrete unpublished data slots and conservative metadata capacity before accepting dirty pages; keep pinned slots safe through bucket emptying. Resume on-disk logged truncates, clear truncated tails, and reclaim orphan extents. Keep read epochs alive through file-data I/O. Validation: eight focused host tests pass, including torn/reordered write recovery, encrypted version reservation, capacity refusal followed by reserved writes, and a pinned bucket's generation transition. no_std clippy passes with warnings denied. Metadata reservation bounds remain conservative; broader regression and pressure checks continue in the integration task.
Correct readdir continuation, retain paged descriptions across unlink, release unheld orphans, and deduplicate sync failures. Adapt fsync tests to real bcachefs transactions: dirty lengths are not published before data. The outstanding large-write capacity regression remains under repair.
Use the existing fileserver EXPORT protocol for inherited file descriptors, including readable stdin. Transfer each local offset to the server before export, and use that shared offset thereafter. Ordinary std Command spawns export a fresh connection each time; forwarding an already exported connection is explicitly one-shot. POSIX file-action dup2 aliases carry a process-local group in their fd label so the child adopts the protocol connection once and shares one descriptor object. Independent launch refuses ambiguous reuse of one raw exported connection instead of attaching it twice. SDK host tests (153), clippy with fd-table, and libc module tests (106) pass. Extend served_file_offset with guest checks for repeated input spawns, dup2 reads/seeks/writes, exact offset sharing and open-unlinked metadata. Guest execution follows the combined storage integration build.
Store the final partial sector as inline data with its suffix zeroed, retaining earlier checksummed slices and removing all later extents. This keeps shrinking possible after ordinary data capacity is exhausted and makes logged-truncate recovery independent of obtaining a fresh data block. Enable the format's inline-data feature and account inline sectors in the inode. Refuse overflowing reservation offsets before adding the block size. Add coverage for full-capacity truncate, imported extent slicing, reserved I/O failure, and the maximum-offset boundary. All twelve focused data tests pass, including plaintext/encrypted torn-write recovery. The per-token metadata reservation cost remains an unresolved integration blocker.
All ToyOS filesystem paths now use the process namespace in userland. Remove the platform dispatch reexport of the deleted kernel-versus-served routing predicate.
Box the mounted probe result, use a concrete flush-future type, and clean affected file-server lint findings. Empty direct writes take no reservation, and metadata failure cancels an accepted reservation. Validation: fileserver library clippy with warnings denied passes.
The file stays readable after unlink and description close while a holder remains, and its pages and inode are released after the final holder notice.
Replace kernel pathname probes with ordinary SDK filesystem operations. Root hierarchy checks use controlled namespaces, copyout probes use generic objects, and directory pagination checks cover complete served listings. Remove the retired backing-revocation actuator registration and its oracle. Keep process cwd within the bootstrap path bound and refuse oversized input before normalization. Include a complete-listing probe in the shared boot now that it no longer exhausts a kernel filesystem inventory. Validation: SDK 153 host tests, metal 81 unit tests, SDK fd-table clippy, focused path boundary tests, and host guest-harness compilation passed. Guest compilation and execution require the consolidated sysroot/image.
Served fstat now reads authoritative filesystem metadata, so paged objects no longer need a timestamp, Stamp operation, or holder metadata syscall. Remove that path from the kernel, ABI, SDK and filesystem mirror while preserving size, atomic read versions, write spans and authoritative Volume timestamps. Pager create now takes backing kind after size. Namespace syscall 166 and the remaining paged operation numbers stay stable. Keep size-coherence tests and remove only assertions about the obsolete timestamp mirror. Include three mechanical file-server binary lint fixes uncovered by all-target clippy. Validation: SDK 153 and ABI 49 tests, paged 18 tests, both bare-target kernel clippy checks, SDK fd-table clippy, file-server binary check and all-target clippy passed. File-server tests 99 passed; its existing 64-page capacity test still fails with NoSpace before touching the pager. No guest image built here.
Allow an in-memory directory to replace an empty directory. Update each shared directory description once during rename, retaining removed destination descriptions and moving source aliases together. Migrate the identity guest test from implicit kernel directories to served directory lifetimes. Validation: file-server memory and description host tests pass. Guest execution follows the integrated image build.
Let the file server enforce dirty-page limits; cp no longer forces fsync every megabyte. Keep the final sync before destination replacement to report delayed write failures. Update the memory benchmark label and sparse test-device description for userland storage. Validation: toybox copy host tests pass.
The removed suites use only the deleted BCFS/BTND API. Real bcachefs operations, I/O failures, reservation pressure, and crash recovery are tested in engine/tests and the served DATA tests. Keep all upstream-generated format fixtures.
Capture adjacent siblings as one bounded rewrite, publish their replacement pointers after all bytes complete, and transfer later changes by key range. Keep still-underfull replacements queued until their siblings can compact. Collapse a one-child root through the journal and ignore obsolete higher levels during replay after collapse. Expose the same relocation path for metadata bucket evacuation and share the minimum fill calculation with capacity admission. Existing conservative reclaim reserve assumptions remain separate integration work. Validation: four host tests cover repeated create/delete churn, clean-node relocation, changes between merged writes and pointer publication, and torn/reordered crash recovery through merges and root collapse.
Prepare shared capacity accounting by maintaining non-structural leaf bytes, BTREE bucket count and imported non-root node debt across applied changes. Allocation index and metadata-backpointer positions have finite bounds from device geometry; the other records are charged from their encoded lengths. Report space excluding outstanding metadata promises. This prepares the occupancy-based reservation ledger; admission still uses the earlier conservative per-token bound pending rebalance and bucket evacuation integration. no_std cargo check passes.
Hold exact data sectors while pending pages charge their enforced journal growth bound. Prepay geometry-bounded allocator records and account live non-structural records; include imported underfull nodes until rewritten. Sparse closed metadata buckets can move through the existing node writer. Keep a reservation until transaction application succeeds so failed preparation retains the accepted page's capacity. Authenticated version reservation needs no additional leaf-key growth. The 64 MiB pressure test accepts 1200 pending pages and writes all of them after allocation refusal. Twelve data tests and no_std clippy pass. Worst-case shared reclaim sizing and GC pressure integration remain under active review; this checkpoint does not claim that physical proof complete.
…as cleaned at Found measuring r5's ring change: with DATA's journal ring at 32 MiB instead of 1.3 GiB, osbench's sequence at full repetitions on the host model took 179 s of data writes instead of 40 s. The sample put the time in Inner::evict's VecDeque::retain. A block cleaned again was queued again while its older entry stayed, and retain kept every entry of a clean block, so blocks written over and over (the journal's, now that the ring is small) left the queue longer than its compaction bound after each compaction, and every eviction compacted it again. The stale entry also evicted a block that had been cleaned since. An entry now carries the version its block was clean at; only the entry matching the block's current version is live, so each clean block has one, compaction leaves the queue the size of what it orders, and the oldest place evicts. The same run now takes 34.7 s. Test: block 1 cleaned after block 2 was read survives the eviction block 3 causes, and 100 000 rewrites of two blocks leave the queue within its bound; red when an entry is live by its block's cleanness alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ap goes r5 BLOCKER 1. Holding live entries to 32 MiB bounded the replay window, but a mount still read and checksummed the whole ring mkfs laid out at a 128th of the device: about 1.3 GiB on the T14's DATA, 32 GiB on a 4 TB disk. mkfs now lays the ring out at a 128th of the device, at least eight buckets and at most JOURNAL_BYTES (32 MiB), never under what reclaim's shares need. The ring itself is then the bound on what a mount reads and what a replay holds, and the writer's own cap (Writer::hold_live_to, its `live` count and the mount's call) is redundant on every volume this engine formats; it is deleted. What stays for volumes another writer laid out: the scan's floor. An upstream ring can be far larger than any window, and a read-only mount of such a volume would otherwise hold every stale entry's records. Its doc now says so. A ring another writer laid out larger is used whole by this writer when mounted read-write, as that writer sized it. r5 suggestions: close asserts each entry's last sequence against the newest closed entry's, not only the newest written one, so the invariant the scan's refusal relies on fails at its source; the scan test gains a stale entry in a later bucket than the window, which takes the at-insert path; the scan's doc is reflowed. Tests: on an 8 GiB device, whose 128th is 64 MiB, mkfs's ring is JOURNAL_BYTES, and after the writer has gone round it an unclean mount's window is within it, fsck clean; red with the ring sized by the device. The wrapped scan case is red without the at-insert drop. The 171 GiB geometry test asserts the bound where it used to assert a 128th. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r6: #1152
|
BLOCKER 1, by the coordinator's ruling: a ring another writer laid out larger than mkfs would for the same device was used whole by this writer, so an unclean mount of, say, a DATA formatted by upstream's tools would replay whatever that ring held. writable() now refuses it read-write by name, "writing a volume whose journal is larger than mkfs lays out", with mkfs's sizing factored into mkfs::ring_buckets so both read one rule. Read-only mounts keep the scan's floor. Test: a 64 MiB volume whose superblock names one ring bucket more is refused read-write by that name and still read; red with the check removed (the mount opened). Suggestions: the 8 GiB test is now what it can show, the writer going round a ring of 2 MiB buckets and an unclean mount replaying it clean (the window bound held by construction); each fetched block takes a fresh version, so only its newest place in the eviction order is live. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… reserving takes no transaction in an open bucket osbench at 6ff184a: seq_write.4096 18 MB/s on both benches (main 133-211, Linux 624-742), write_4k_at 25-30 us (main 10-12). Measured on the host model of the T14's DATA before fixing, 4 KiB calls through the file server's reserve and its dirty-page write-back: reserve 8.7 us a call, write-back 23.6 us a page (1 MiB calls: 2.9 + 2.6) Every call was its own reservation: a transaction to take its sectors, and at write-back one transaction and one extent per page, since pages under different reservations are written apart. Now: - Mount::extend reserves the blocks past a reservation's end into it; where the new sectors follow the last run's in the same bucket, they join that run (up to the 64 KiB extent), so a write-back of both is one extent. Mount::reserve is an empty reservation extended. - Sectors of the open data bucket are taken in memory when the grant's own room judgement (Mount::room_for, what grant refuses NoSpace without) holds; a new bucket, or a judgement that may wait or refuse, is still a transaction's. - The file server's DATA volume extends the last write's ticket when a write starts where its reservation ends; a refused write cuts the extension back (Mount::cut), and an unreserve that landed nothing leaves the ticket's earlier pages alone. Same measurement now: reserve 0.9 us a call, write-back 3.6-3.9 us a page. Scattered 4 KiB overwrites (write_4k_at's shape): reserve 0.8 us, the overwrite's transaction itself 30.6 us; the latter is the copy-on-write replace of the old extent, unchanged here. Tests: extending a reservation block by block writes back as at most one extent per bucket of four blocks (64 extents without merging, red) and a cut gives back the last extension; on 2 MiB buckets a run never joins across into the next bucket (red without that condition: the harness had hit "data placed in a bucket its alloc key holds otherwise"); cancelling a write that extended the last one gives back its room (red without the cut). The partial write-back test writes its tail first so its two pages stay under two writes' capacity, as it states. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings #1179 (every guest test runs on the guest runner). No conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
By the coordinator's decision #1152 lands with these recorded, not fixed. osbench at a1f0762, both benches, against nightly 2026-10-31-2316: - write_4k_at about 2.4x main: the copy-on-write overwrite transaction, 30.6 us of host CPU an overwrite, spread with no single cost. - readdir.1000 about 3x main: each listing reads every entry's inode with a lookup of its own (416 of 534 us on the host model), and every READDIR request lists the whole directory again. Each has its evidence, owner and exit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
By the coordinator's ruling under no silent debt: tree.100 (1.41-1.53x), stat, fstat and their direct variants (1.17-1.26x), and seq_write.4096 on the AMD (0.78x) at a1f0762, with what is known of their causes, an owner, and the exit "within 1.1x of main on both benches, or each row's cause shown to be inherent". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r7: #1152
|
… or refused, each take judged for its credit BLOCKER 1: the journal-size refusal sized mkfs's ring, which builds a journal writer for the volume's buckets, before the shape refusals; a volume of 8 KiB buckets, which a reader accepts, reached Writer::new's assert in the file server. The check now comes last in writable(), after the refusals that rule such buckets out. The shape test gains an 8 KiB- bucket case, refused by name (it panicked before the move). BLOCKER 2: an extension refused partway kept the runs it had taken, pinned with their credit, and the file server's tail range grew though the write was refused. Mount::extend now cuts the reservation back to where it started on any error: whole or refused. Test: near full, an extension of 256 KiB refused NoSpace leaves the reservation's range and the volume's room as they were; red without the cut (the range grew by 96 KiB and the room fell to 0). BLOCKER 3: a take was judged for one transaction's credit and could promise sixteen blocks, each owed a transaction's credit as each may be written back alone. Each take is now judged for its blocks' credit: in memory by room_for, and on the transaction path, whose grant judged one, by room_for for the rest inside it, refused NoSpace where it does not fit. An assert holds every take to the credit judged for it; with the old judgement it fails in every multi-block reservation test. A near-full test that would show an over-promise directly could not be built: near full, the space judgement is node-granular and the binding limit is data room, so the at most fifteen credits a single take over-promised stayed under one node there. BLOCKER 4: room_for and its doc sit above grant's doc again. Suggestion: extended_from has its own doc line. Unchanged cost (host model): 4 KiB calls reserve 0.9 us and write back 4.1 us a page; scattered overwrites 30.8 us. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings #1180 (bootstrap gets a $TMPDIR of its own). No conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r8: #1152
|
r7 B3 continued. A take that opened a bucket went through a transaction whose grant judged and held one transaction's credit; the take's extra credit was then judged inside it against today's room, while data_space's own check for the new bucket saw only the grant's one credit, so neither judged both the take's credit and the bucket it lost; and the inside check refused at once where grant waits until settled. Now admission holds a metadata credit apart from the journal bound: admit, grant, ungrant, prepare_pending and finish carry it, granted_bytes counts it, and Mount::transact_holding grants a transaction its bound's records and a credit of at least them. transact is transact_holding at its bound's bytes. A take on the transaction path is granted its whole credit, so data_space's bucket check sees it and a refusal follows grant's settled rule; the inside check is gone. The in-memory take from the open bucket, which costs no capacity, stays judged by room_for for its whole credit. Evidence: the circular assert is replaced by the invariant itself, in tests, after every take: everything promised still fits (room_for(0, FOOTPRINT)). A near-full test fills the 64 MiB image (32 KiB buckets, eight-block runs) with 32 KiB and with 16 KiB reservations and writes every block back; red when the grant holds one transaction's credit for a take (the invariant fails). Reverting the in-memory judgement to one credit stays green on this image: there a bucket is eight blocks and so is a metadata bucket's worth of credit, and the fills tried end credit-bound with the open bucket full, or, with blocks written first to offset it, bound by data buckets with credit to spare; no buildable state put an in-memory take across the boundary. Suggestion: extended_from's doc says only where the tail's reservation ended before the current write extended it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r9 of #1152 at 9fe8f77: B3 onlyScope. This round checks only r8's BLOCKER 1 (r7 B3 continued), at 9fe8f77: the fix diff, its callers and the r12-review-r8 evidence. Architecture. The fix follows the one-mechanism design from r8. Admission now holds the take's whole metadata credit in
The in-transaction check is deleted. Production code changes by 25 lines added and 27 removed, and the commit adds one 38-line test. That is proportionate. B3: resolvedTransaction path (
In-memory path (
Lifecycle of the credit.
Evidence.
FindingsNone blocking. SUGGESTION. LAND |
Both were red on both benches at 9fe8f77 and not on the K4 baseline. - pipe_other_end_gone: this branch replaced its kernel file handle, an object with no other end that answers being readable, with a memory object when the kernel's File object went. A memory handle has no WAIT right, so the watch is refused PermissionDenied before the kernel asks whether the object has an other end, and the test's NotSupported never comes. The case now uses an event's wait side: watchable, readable when raised, and no pipe end. The kernel's order (rights first, then whether anything could answer) is right: a holder without WAIT learns nothing about the object. - 207_libc_names: readdir on /tmp gave DT_UNKNOWN for regular files under the kernel's tmpfs; /tmp is now the file server's memory volume, whose listing carries each entry's kind, so libc reports DT_REG. DT_REG is right: POSIX leaves d_type to the implementation, and Linux's tmpfs, ext4 and the rest report DT_REG for a regular file. The case prints DT_REG by name rather than "another type", and its expectation says it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The judge failed readdir_bound on both benches at 9fe8f77: "no exit record, never ran". f916f41 moved it off its own boot (testcases- readdir, needed while it filled the kernel's /tmp to its listing limit) onto TESTCASES, but kept it in RUST_SKIP and never added its job to that arm, so nothing ran it. On the K4 baseline 8ac4f21 it had its own boot, which that run did not stage either. The test now cleans up after itself on the served /tmp, so it needs no boot of its own: its RUST_SKIP entry and its explicit row go, and it is a discovered member of the shared boot, judged on its exit like the others. The Arm doc's example of a test that must not share named it; it now says the rule without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r10 of #1152: 9fe8f77..6fd7151 (two testcases members)Scope. This range changes three test files and nothing else (+15 −10). The metal reds at 9fe8f77 are recorded at evidence Architectural assessment1.
2.
Findings
SEND BACK |
The event's wait side came from the raw event_create, adopted under unsafe; toyos::wake::pair() gives the same handles typed and safe, and the Bell is the wait side the poller watches. The import change and the manual drop go; the comment states only the contract. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r11 of #1152: 6fd7151..57dedf1 (test registration and one test)Scope. Two commits change three test files, +8 −21. No production code changes. Architectural assessment
1. The r10 blocker is resolved
2.
|
…ock (S0-a) Every user copy goes through `arch::uaccess`: on x86-64 `stac; rep movsb; clac` (or `rep stosb`), on AArch64 unprivileged LDTR/STTR. A Ring 0 or EL1 fault at one of those accesses on a user address is served as the process's own fault would be and the copy resumes, or the copy resumes at its fixup and answers the bytes it did not copy, which `user_ptr` reports as `Faulted` (`BadAddress`). Each routine clamps an address at or past the user half's top to 0 without a branch (`cmovae`; `csel` plus `csdb`), so a mispredicted range check loads no kernel byte. No user copy runs under a spin lock: `user_ptr::gate` panics above the syscall's preemption depth or with interrupts masked. The kernel has no sleeping lock since #1152, so that is every kernel lock. Callers clone their object out of the process's lock (`syscall::handles::object`) and copy after; a device claim is described under it and copied out after (`DeviceRead`). Pipes and messages share one claim protocol whose waiter records itself each time it finds the claim held (`kernel::pipe::Bytes::turn`), so no wake is lost, and an abandoned write announces its room. A pipe transfer of at most 4 KiB and every message bounce through the caller's stack in the hold that looks the handle up; a larger pipe transfer claims its bytes and copies straight between frames and the user. The SDK's `lock_word` spins briefly before it sleeps. SMAP and PAN being required, and the entry `clac`, are main's (#1154). The kernel-file and console parts of the earlier rounds went with #1152. Closes issues/a-user-copy-demand-pages-under-whatever-its-caller-holds.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ock (S0-a) Every user copy goes through `arch::uaccess`: on x86-64 `stac; rep movsb; clac` (or `rep stosb`), on AArch64 unprivileged LDTR/STTR. A Ring 0 or EL1 fault at one of those accesses on a user address is served as the process's own fault would be and the copy resumes, or the copy resumes at its fixup and answers the bytes it did not copy, which `user_ptr` reports as `Faulted` (`BadAddress`). Each routine clamps an address at or past the user half's top to 0 without a branch (`cmovae`; `csel` plus `csdb`), so a mispredicted range check loads no kernel byte. No user copy runs under a spin lock: `user_ptr::gate` panics above the syscall's preemption depth or with interrupts masked. The kernel has no sleeping lock since #1152, so that is every kernel lock. Callers clone their object out of the process's lock (`syscall::handles::object`) and copy after; a device claim is described under it and copied out after (`DeviceRead`). Pipes and messages share one claim protocol whose waiter records itself each time it finds the claim held (`kernel::pipe::Bytes::turn`), so no wake is lost, and an abandoned write announces its room. A pipe transfer of at most 4 KiB and every message bounce through the caller's stack in the hold that looks the handle up; a larger pipe transfer claims its bytes and copies straight between frames and the user. The SDK's `lock_word` spins briefly before it sleeps. SMAP and PAN being required, and the entry `clac`, are main's (#1154). The kernel-file and console parts of the earlier rounds went with #1152. Closes issues/a-user-copy-demand-pages-under-whatever-its-caller-holds.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What this changes
Replace the private BCFS/BTND DATA format with the native bcachefs engine. Move the RAM-backed filesystem into the userland file server and remove the kernel VFS, tmpfs, pathname syscalls and associated file cache. Rust std and libc resolve files through the capability namespace; the kernel receives executable memory objects instead of opening paths.
The branch also preserves served descriptor ownership across spawn, obtains authoritative file metadata from the server, adds native DATA writes, sparse-bucket evacuation and reserve-protected reclaim (an ordinary transaction's keys go into the leaves it holds, as upstream bcachefs does; replayed keys and reclaim's own updates wait in pending runs), and provides an explicitly authorized
disks format data --commitpreparation path with a two-boot persistence test.Why
Persistent DATA must use the format the crate claims to implement, and filesystem parsing and pathname policy belong in userland. This consolidates the existing storage paths rather than retaining both implementations.
Evidence
Round 4 (
94fdd7faa, on the root-cause fixes938f1e701..164f391a5) — https://github.com/ToyOSOrg/evidence/tree/18bed1cfe6cb3bd86e0aca385445df8be0680a60/storage-performance and https://github.com/ToyOSOrg/evidence/tree/b30cf0b9c0e2adbb8882674b8a306f543d4247b8/data-mixed-volume:Overwritten), checksum-none metadata is refused, mkfs erases its copies' reserves and the tail, replay tests cover the benches' states.cargo run -- --ci hostat164f391a5: exit 0, 109 steps.bench_data_formatted(disks format data --commit) at164f391a5passed on both benches: each found the old contents, destroyed them and mounted the new bcachefs volume. Both benches' DATA now hold this branch's format.cargo test --release -p bcachefsat94fdd7faa: exit 0, 186 passed; clippy (no default features,-D warnings) exit 0;cargo run -- --ci host: exit 0, 109 steps. Nine negative controls, each applied and restored by script: eight red, one (a truncation cursor) green, which showed that code redundant; it was removed before the commit.Earlier rounds:
Integration checkpoints
f32a98839,db6cab94a(maine3a421563),7987307ff,e7f2a85b0(#934 merged in)f14f79e50(geometry fix) andc12721aa1(R1 fix, 16 KiB floor, review r2 S1/S3) (sanitized logs, README per file: https://github.com/ToyOSOrg/evidence/tree/181e5b9460dc61805cf2b216cecd20fd27f7dd22/storage-integration):cargo run -- --ci host: 109 steps green at each checkpoint; the latest, exit 0, atc12721aa1.cargo run -- --ci image(x86-64 and AArch64): exit 0 atf32a98839,db6cab94a,7987307ffande7f2a85b0.cargo test -p bcachefs --libatc12721aa1: exit 0, 180 passed; no-default-features clippy exit 0.libc_children(QEMU x86-64) ate7f2a85b0: exit 0.process_tree,lseek_past_eof,fs_truncate_persist,spawn_from_tmp,abuse_elf_loader) build for x86_64-unknown-toyos with no warnings; none is booted by a run above.cargo test -p bcachefs --libat4e829b244(reclaim integrated): exit 0, 169 passed;cargo clippy -p bcachefs --lib --no-default-features -- -D warnings: exit 0.libc_children(QEMU x86-64,tests/images/libccase.toml) at25f19a076plus the then-uncommitted served-stdout case, identical to48316fc0e's: exit 0, every member including the newspawn_served_stdout. Negative control:toyos::fd::copyrefusing a served file builds, and onlyspawn_served_stdoutfails, atsystem, errnoEOPNOTSUPP.fileserver'sa_write_a_gibibyte_past_the_end_leaves_a_hole(in the host run): passes on a 64 MiB volume, which no bridged gap could fit.Earlier evidence, by checkpoint (not rerun; raw logs in local scratch, not yet published):
cargo run -- --ci hostat63783da5d: exit 0, 109 steps.cff9c3ebd: explicit native DATA formatting, write/sync, reboot and read-back;bench_data_formattedandbench_data_survives_rebootgreen. Upstream bcachefs-tools v1.39.7fsck -n -Kexit 0 on both benches' DATA. Plain and encrypted host artifacts passed upstream fsck and independent FUSE reads, including 1,600 retained sparse files.605c7c3cb: 14 Rust/C file, descriptor and pager tests plus launch, badge and app-view checks, 17 green.0249f7f23content): whole-removal, metadata-full and DATA-full reopen, low-journal-headroom crash oracle (plain and encrypted, 8 cut points × 4 variants), the floor-disabled control failing at removal 865, and provenance negative controls. Independent delta review found no further confirmed defect. Integrated here as11632e8b6and4e829b244, with two conflict hunks (a doc comment andcan_free's terms).Landing boots at 57dedf1, judged member by member
Evidence: https://github.com/ToyOSOrg/evidence/tree/ea7487da78572a00b9436afe13e3a4260b225f35/storage-integration/r15-member-judging. The coordinator's proctreecase, testcases and testcases-watchdog boots on both benches, judged by the staging command run again with the readbacks in place: T14 270 passed, 6 failed; AMD 269 passed, 7 failed.
test_rs_pipe_other_end_gone,test_rs_readdir_boundand207_libc_namesexit 0 on both. The reboot pair's evidence at9fe8f779estands (only tests changed since).Every remaining red is cross-bench or pre-existing, each red on the K4 baseline
8ac4f2187too:idle_entered,amdvi_translates_the_boot_controller,amdvi_remaps_the_boot_controller,amdvi_usbd_drives_its_own_entry(AMD rows) andconsole_uart_driven(no COM1, and that AMD row's panic) —issues/a-metal-row-names-no-machine-so-another-benchs-boot-carries-it.md;i219_messages_on_its_msi_vector—issues/i219-msi-passes-only-when-the-network-sends-it-a-frame.md.acpi_server_events,battery_read,usbd_drives_the_type_c_controller,i219_messages_on_its_msi_vector(the T14's rows) andconsole_uart_driven— the same cross-bench record andissues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md;claim_reuses_its_remapping_entryandcounters(its judge requires an HWP declaration the AMD bench has not) —issues/a-metal-row-names-no-hardware-and-a-bench-without-it-reds.md.Round 13: two testcases members
Evidence: https://github.com/ToyOSOrg/evidence/tree/ac3e8b1969e08e85ff63e58ea82d9426e43c0e09/storage-integration/r13-testcases-members. The landing boots at
9fe8f779epassed as boots, but two testcases members were red on both benches and not on the K4 baseline (6fd7151b6fixes both tests):test_rs_pipe_other_end_gone: this branch had replaced the kernel file handle (removed) with a memory object for "an object with no other end". A memory handle has noWAITright, so the kernel refuses the watchPermissionDeniedbefore asking whether anything could answer it; the test expectedNotSupported. It now uses an event's wait side (toyos::wake::pair()'sBell,57dedf1f3), which is watchable and readable and is no pipe end. The kernel's order is right: a holder withoutWAITlearns nothing about the object.207_libc_names:readdirof/tmpgaveDT_UNKNOWNfor regular files under the kernel's tmpfs; the file server's memory volume carries each entry's kind, so libc now reportsDT_REG. That is right: POSIX leavesd_typeto the implementation, and Linux (tmpfs, ext4) reportsDT_REG. The case namesDT_REGand expects it.verdict.txtis the boot's own verdict by design; the members are judged by running the staging command again with the readbacks in place, which reds both (offline judgement in the evidence).readdir_boundalso failed the judge on both benches ("never ran"). This branch'sf916f41edhad moved it off its own boot onto the testcases arm but left it inRUST_SKIPand out of that arm's jobs, so nothing ran it; the K4 baseline gave it its owntestcases-readdirboot, which the baseline run at8ac4f2187did not include either (test_rs_readdir_boundappears nowhere in its T14 or AMD testcases log), so the baseline has no result for it. Since the test now cleans up after itself on the served/tmp,275a9e6b8makes it a discovered member of the shared boot again (its skip entry and explicit row removed).Round 12: review r8, a take's whole credit granted
Evidence: https://github.com/ToyOSOrg/evidence/tree/c00a7606e1de3ea0be69a5d0b7342c7f1dade0b7/storage-integration/r12-review-r8.
cargo run -- --ci hostat9fe8f779e: exit 0, 108 steps. AArch64 rows atc4d1db6e2(runner run 41947682423): 5 passed, all green; rerun at9fe8f779e: run 41953629015.9fe8f779e, the reviewer's design): admission holds a metadata credit apart from the journal bound (Mount::transact_holding;transactis it at its bound's bytes), counted ingranted_bytes. A take that opens a bucket is granted its whole credit, sodata_space's bucket check sees it and a refusal followsgrant's settled rule; the in-transaction check is gone. The in-memory take from the open bucket stays judged byroom_forfor its whole credit.room_for(0, FOOTPRINT)). A near-full test fills the 64 MiB image (32 KiB buckets, eight-block runs) with 32 KiB and 16 KiB reservations and writes every block back; red when the grant holds one transaction's credit for a take. Not shown by a control: reverting the in-memory judgement to one credit stays green on this image; fills tried ended credit-bound with the open bucket full, or data-bound with credit to spare, so no built state put an in-memory take across the boundary. That path's judgement isroom_forof the take's whole credit, the same rulegrantapplies.extended_from's doc.Round 11: review r7
Evidence: https://github.com/ToyOSOrg/evidence/tree/f091fd2a11bb0304b37e82a5c90812e97650106d/storage-integration/r11-review-r7.
cargo run -- --ci hostatc4d1db6e2(maineae2c7eb7, #1180, merged cleanly): exit 0, 108 steps.79cd415d4): the journal-size refusal sized mkfs's ring before the shape refusals, so an 8 KiB-bucket volume reachedWriter::new's assert in the file server. It is now the last refusal; the shape test has an 8 KiB-bucket case (it panicked before the move).Mount::extendis whole or refused: on any error it cuts the reservation back to where it started. Test near full, red without the cut.room_foris abovegrant's doc again. Suggestion:extended_fromhas its own doc line.a1f07627e(runner run 41941136592): 5 passed; the job was red only on a leftover in its$TMPDIR(rustix_test_can_compile, a build-script probe), which build: bootstrap gets a $TMPDIR of its own, gone when it returns #1180, now merged, addresses. Rerun atc4d1db6e2: run 41947682423.Where it stands against main, and why it lands anyway
osbench at
a1f07627eon both benches (coordinator's runs;osbench_persistexited 0 on both), against the same nightly as before (maindc1a8642eon its private format, Linux ext4). p50, one run a side. Evidence: https://github.com/ToyOSOrg/evidence/tree/760ea485db3b1b2016f7c1748901c352c146449a/storage-integration/r10-known-gaps.Behind main:
write_4k_at2.35x (T14) / 2.52x (AMD): each 4 KiB overwrite is a copy-on-write replace transaction (30.6 us of host CPU, spread with no single cost). Recorded:issues/a-4-kib-overwrite-on-data-costs-about-2-4-times-mains.md.readdir.10003.15x / 2.87x: a listing reads every entry's inode with its own lookup (416 of 534 us on the host model) and each READDIR request lists the whole directory again. Recorded:issues/a-data-directory-listing-reads-every-entrys-inode-alone.md.tree.1001.53x / 1.41x,fstatanddirect.fstat1.22-1.26x,statanddirect.stat1.17-1.20x, and on the AMDseq_write.40960.78x (164 against 211 MB/s; the T14 is 1.22x). Recorded together:issues/smaller-data-rows-trail-main-by-up-to-half.md.Why it lands anyway: reads through the paged object are 8-13x faster than main (
read_4k,pread_4k,pread_8,read_4k_at0.07-0.15x of main's time;seq_read.40969.6x and 15x the throughput), metadata is near main (open_close 0.92-1.09x, rename and mkdir_rmdir at or under, create_write_unlink 0.79-1.06x), and sequential writes are at or above main (seq_write.10485761.30-1.32x;seq_write.40961.22x on the T14).fs.disk at a1f0762 (p50)
T14
AMD
Round 9 (
49fe47993) tookseq_write.4096from 18 to 162/164 MB/s (6ff184a's rows inr9-small-writes/osbench-6ff184af8/, a1f0762's inr10-known-gaps/osbench-a1f07627e/); the other rows match6ff184af8's within run-to-run spread.Round 9: small writes, and r6's refusal
Evidence: https://github.com/ToyOSOrg/evidence/tree/91cd6d4b3ae9724e6c92912ad63b182e7c3c707b/storage-integration/r9-small-writes.
cargo run -- --ci hostata1f07627e(main116dde5de, #1179, merged cleanly): exit 0, 108 steps. AArch64 rows: guest runner, run 41941136592.6ff184af8(format, then benchcase, both benches; coordinator's runs):osbench_persistexited 0, so the post-replay hang is gone on metal. Table below against the same nightly as round 5 (maindc1a8642e, Linux ext4). Metadata rows are now at or under main's (stat 1.17-1.18x, open_close 0.93-1.08x, rename 0.52-0.65x, create_write_unlink 0.79-1.06x); readdir.1000 stays about 3x main. Small writes were clearly worse: seq_write.4096 18 MB/s (main 133-211, 0.09-0.14x), write_4k_at 25-30 us (main 10-12, 2.5x).49fe47993). Host model, 4 KiB calls through the file server's reserve and dirty-page write-back: reserve 8.7 us a call (a transaction to take sectors) and write-back 23.6 us a page (each call its own reservation, so one transaction and extent per page), against 2.9 + 2.6 us per page in 1 MiB calls. Now a write that starts where the last write's reservation ends extends it (Mount::extend; sectors following on in the same bucket join the last run, up to the 64 KiB extent), and the open bucket's sectors are taken in memory when the grant's own room judgement holds. Same measurement: reserve 0.9 us, write-back 3.6-3.9 us a page. Scattered 4 KiB overwrites (write_4k_at's shape): reserve 0.8 us, the copy-on-write replace transaction itself 30.6 us, unchanged; the sample shows it spread over node key copies, the pending overlay and allocation, no single cost. Whether write_4k_at now nears main is for the staged osbench. Controls: no run merging (64 extents for 64 blocks), runs joining across buckets (fsck and read red), no cut of a refused extension (room not returned).cd639c3e6, by ruling): a read-write mount refuses a volume whose journal is larger than mkfs lays out (Unsupported("writing a volume whose journal is larger than mkfs lays out")), mkfs's sizing shared asmkfs::ring_buckets; read-only keeps the scan's floor. Test with control (the mount opened without the check). Suggestions: the 8 GiB test is now the 2 MiB-bucket wrap, replay and fsck test; each fetched block takes a fresh version.fs.disk at 6ff184a (p50; one run a side)
T14
AMD
Round 8: the journal ring is the replay bound
Evidence: https://github.com/ToyOSOrg/evidence/tree/770c5c18ca69e7e8317ddb0d0aa1a130ae5569b4/storage-integration/r8-journal-ring.
cargo run -- --ci hostat6ff184af8: exit 0, 108 steps. AArch64 rows: guest runner, run 41934369401.6ff184af8): round 7's writer cap bounded the window but a mount still read and checksummed the whole ring mkfs laid out at a 128th of the device (1.3 GiB on the T14). mkfs now lays the ring out at a 128th, at least eight buckets and at mostJOURNAL_BYTES(32 MiB), so the ring itself bounds what a mount reads and a replay holds. The writer's cap is deleted. The scan's floor stays: a ring another writer laid out (an upstream volume, read-only or not) can hold far more than any window. The ring sizing is a format change for volumes this engine makes, so each bench's DATA is reformatted first.JOURNAL_BYTES, and after the writer has gone round it an unclean mount's window is within it; red with the ring sized by the device.closeasserts against the newest closed entry's last sequence; the scan test gains a stale entry in a later bucket (red without the at-insert drop); the scan's doc is reflowed.4410a5664, the file server's block cache, on main too): with the small ring, the full-repetition host model's data writes took 179 s instead of 40 s. The eviction queue kept a stale entry for each block cleaned again, so blocks rewritten over and over (now the journal's) kept the queue past its compaction bound and every eviction recompacted it; the stale entry also evicted recently cleaned blocks. Entries now carry the version their block was cleaned at. Same run: 34.7 s; mount 0.20 s, first mkdir 0.024 s, reader peak 0.14 GB.Round 7: r4's blockers, and the replay window at full scale
Evidence: https://github.com/ToyOSOrg/evidence/tree/c7e23b6741df00466ed59cafb0e824a3a60f5b66/storage-integration/r7-replay-window. Main merged twice (
aecd87eca: #1176 and #1174, one modify/delete conflict, the swapped-binary issue this branch closes stays deleted;62e24c902: #1178, clean).cargo run -- --ci hostat62e24c902: exit 0, 108 steps.bdf062cd3): the writer holds live journal entries to 32 MiB of the ring (JOURNAL_LIVE), and the scan keeps records only for entries that can be in the window (an entry behind a good flush entry's last sequence keeps none; a window reaching one is refused). Full scale now: peak 0.20 GB, mount 1.10 s, first mkdir 0.018 s. The record closes by its exit. Tests with red controls: a writer held to 3 of 8 buckets behaves like a 3-bucket ring; a scan keeps only the window's records.f433570e3): the doc comment is back on its test; the name cache charges each entryENTRY_BYTESbeyond its path;MAX_KEY <= K_TX * 8is a compile-time assert; the superblock test's doc is reflowed.virt_jobs_at_el2,virt_smp,virt_user_mode,virt_readonly_copyout,a_bent_supervisor_is_refused_and_the_other_slot_boots) through--ci rowsataecd87ecaand62e24c902: 5 passed, 0 failed each time, but HOST NOT QUIET (2.2 CPUs of other work, the desktop's own processes), so no verdict.Round 6: the post-replay hang, and r3's blockers
Evidence: https://github.com/ToyOSOrg/evidence/tree/9dfdd8af0ef226d304024aa61a9b3781fbed0c37/storage-integration/r6-replay-hang.
cargo run -- --ci hostata0098f4d4: exit 0, 108 steps. QEMU x86-64 rows (the DATA designation,nvme_disk_boots, the host disk left alone,kernel_objects_refused_alone, the image spawn) ata043c5e4b: exit 0, 5 passed.45283fbc5in its first DATA mkdir after the reboot. Reproduced on a host model of the T14's DATA: osbench's call sequence, dropped unclean, replays about 1.5 million journal keys, most of them accounting deltas at a handful of positions. The first transaction's leaf drain transferred them one at a time, each transfer walking every entry at its position (about 0.45 ms a delta).c836688ce: a node takes a position's deltas in one pass; the same image's first mkdir now takes 0.33 s. Test: the first transaction after a replay looks at no more than 16 pending entries per replayed one (the old transfer: 262 657 for 1 551, red).K_TX, so the "two transactions more"MAX_GROUPSassumes holds as written. Test: renames over files and creates after a replay grow no node pastK_TX * 8bytes (draining for every key: 6 872, red; a room of the whole bound: 4 224, red).af6ae8293): a reserved write's extent points at the block holding its offset. Test on 16 KiB blocks at 4096, neighbour unchanged, fsck clean; the old sector reads asChecksum("extent"), red.dfa6e2166): a whole superblock copy of another volume refuses the volume asOverwrittenat its offset. The test case is a real other volume's copy (before the fix it opened); the flipped byte is relabelled a damaged copy.537af08e7):issues/a-reserved-data-block-holds-a-transactions-metadata-credit-until-written.mdandissues/a-small-bcachefs-journal-makes-reclaim-write-nodes-after-nearly-every-transaction.md, each with evidence, owner and exit.issues/a-replayed-bcachefs-node-past-seven-nodes-is-refused.mdcloses: since11632e8b6replayed keys materialize in bounded ranges. The bcachefs track records that checksum-none volumes are refused.c74862d03): the DATA name cache forgets a path by its sorted range and is bounded by 256 KiB of path bytes.a0098f4d4): the hung boots'verdict.txtreadpassed, since the boot judge read only the boot record and the supervisor's stop.bootlog::verdictnow refuses a log carrying the test runner's job-deadline line.Round 5: write-back by extent, and the served path's lookups
Both flagged causes from the bench table below are fixed; the boots that judge them are staged at
45283fbc5. Evidence: https://github.com/ToyOSOrg/evidence/tree/f43cc46dac51c8ec0f7326433f302afa20d47d2c/storage-performance.cargo run -- --ci hostat45283fbc5: exit 0, 108 steps. Engine suite 189 passed, file server 109; new negative controls each red (unwritten blocks leaked, runs of one block, whole-run allocations closing the bucket, no bound on old extents, the refused suffix reported from 0, names never forgotten).f56cc636b). A reservation is runs of contiguous sectors of at most 64 KiB (upstream's default extent), and a write puts one extent in each run it crosses; the file server keeps one reservation per write and writes each run of pages back at once. The per-page path is gone. An allocation takes what the open data bucket has left (where the extent ends) instead of closing it, which a whole-run allocation did, and which made evacuation and a refused write chase each other forever. Each transaction still removes at most two old extents, so an overwrite of fragmented data goes in parts. Host model, per MiB of a 64 MiB file in 1 MiB calls: 6.8 ms before, 0.66 ms now (main 0.18 ms); 4 KiB calls with no pager stay a transaction each.45283fbc5). Measured first, on a probe (QEMU/KVM): one STAT is one request from the client in both trees, but the server resolved the same DATA path four times through the engine, from the root (11 dirent lookups + 4 inode reads, 41.7 us of a 58.4 us stat). The DATA volume now remembers what each path it looked up names (unlink, rmdir and rename forget what they move), and STAT and OPEN look the name up once. Server STAT 41.7 -> 5.2 us, stat p50 58.4 -> 24.3 us; OPEN 52.5 -> 11.0 us, open_close p50 78.4 -> 36.4 us; fstat, the bare round trip, 17.1 us. Evidence: storage-performance/served-path.8f52abe99:nvme_disk_bootsexpected the private format's line ("bcachefs, "); it failed atf1b2f3e38on jan-pc having seen DATA served.libc_children'stty_interruptstalls at the nested shell's line atf1b2f3e38and at main4610c48b3alike: main'sissues/libc-childrens-tty-interrupt-can-stall-at-the-nested-shells-line.md, not this branch's.issues/a-reserved-data-block-holds-a-transactions-metadata-credit-until-written.md).fs.disk on the benches (94fdd7f)
One
osbenchbenchcase boot a bench at94fdd7faa, against the newest main osbench on the same benches (nightly 2026-10-31-2316: maindc1a8642e, private DATA format) and that nightly's two Linux runs (ext4, p50 median). p50 everywhere; one run a side, so a trend. Evidence: https://github.com/ToyOSOrg/evidence/tree/19709a9e5f559251bf8a661db7fc07b5ac54aad1/storage-performance/osbenchT14
AMD
Flagged:
Regressions this branch had introduced, fixed
SYS_SPAWNnow refuses itInvalidArgument(kernel::cwd::spawnable, host-tested with a negative control;abuse_spawn_argvasks it through the syscall)./tmp. Swap staged binaries on/tmpand the supervisor read them from its loop; the SDK waits on a file server without bound. Nowswaplends the bytes in a memory region, the supervisor verifies its own copy and starts the service from it at every start. No file server is asked, andissues/a-swapped-binary-lives-where-any-process-can-rewrite-it.mdcloses by its "bytes the supervisor hands over" exit shape. std'sCommandExt::boot_imagebecomesCommandExt::image, accepting the caller's own unchanged region too. The owner delegated that choice to the orchestrator, who accepted it. Swap has no behavioural test before or after.Mount::spans_its_devicerestores it at the DATA probe, with a negative control.issues/probe-mounts-on-a-checksum.mdis rewritten against the native code.abuse_spawn_argvandabuse_elf_loaderspawned without an image, so their refusals held vacuously and their started-child cases could not pass; both carry real images now.Process-depth test
process_tree's chain ran each link from a copy on/tmp. With/tmpserved, every link kept its own/tmpconnection, so on the T14 at0507cbe36the chain met the file server's 32-client share bound at link 32 before the kernel's depth refusal at 65. Each link now starts the next from this program's immutable image throughCommandExt::boot_image, endowing it a copy, so no link asks a file server for anything. The depth-65 and subtree-teardown assertions are unchanged. The cap is not raised; the limit/tmpnow shares is recorded inissues/four-shares-take-a-file-server.md./tmpspawn coverage stays (spawn_from_tmp,fs_truncate_persist, B's malformed-ELF refusal).What is unsure
Draft: not ready to land.
8e117f19eandf14f79e50(from the reclaim branch'saaa71f0bb,825b9ba7a) end every clean unmount in one terminal publication; the owner kept one-block nodes writable (issues/upstream-cannot-check-written-bcachefs-volumes-of-one-block-nodes.md). Its review's blocker (an empty appended bset on production-sized volumes) is fixed (248db3d49), nodes are written down to 16 KiB (d5158bd3d), and a mount now judges the superblock it actually runs on (c12721aa1, with a device answering differently on its second read).fs.disk.renamespecifically: paged: a holder's read is atomic against its pager's write; an fsync commits its own file #1141's last A/B flagged it +19.8% on the AMD, unexplained.57dedf1f3: theproctreecase,testcasesand watchdog boots on both benches, judged above; the reboot pair at9fe8f779e. Format at6ff184af8and osbench at6ff184af8anda1f07627epassed on both; the volume format has not changed since6ff184af8, and the engine changes sincea1f07627etouch only refusals and near-full judgement. A main-based image with DATA rows must not boot on either bench until this lands: main's format would mount nothing sane from the native volume.9fe8f779e(run 41953629015); only tests changed since.issues/a-small-bcachefs-journal-makes-reclaim-write-nodes-after-nearly-every-transaction.md)./tmp's file-server cost against the kernel tmpfs it replaces is unmeasured (issues/a-tmpfs-metadata-call-costs-toyos-up-to-six-times-linuxs.md).SYS_YIELD) and 169 (SYS_LOG_LAST_WORDS);SYS_NAMESPACE_NAMESis 170 with 172 bins.ObjectKindloses bothFile(this branch) andConsole(K4), renumbered densely (SysCap9 ..DeviceSet19). Main was merged in rather than rebased (f450182c6): a linear rebase replays paged: a holder's read is atomic against its pager's write; an fsync commits its own file #1141's and paged: a holder's fstat is its object's, without a request (FS stage 3a) #1144's merged-in history onto a main it was never written for and stopped at the sixth of 69 commits; the merge resolves each conflict once against the final states (its message lists them).f1b2f3e38gives this branch's two DATA images K4's UART and logkeeper rows.cargo run -- --ci hostatf1b2f3e38: exit 0, 108 steps.Anything a reader of main must not miss
mainbranch and pinned by this branch's gitlink. It removes std's reexport of the deleted SDKis_servedpredicate. Review the fork delta and SDK change together here, following the workflow used in handles: only an owner ends, moves, duplicates or lends a handle #1080, files: one client of the file servers, in toyos, for std and libc alike #988 and sdk: the transport is toyos::transport, and no crate of its own #972. The unnecessary separate Rust PR was closed without merging.f32a98839's message;bcachefs-crate-is-not-bcachefsis renamedthe-bcachefs-crate-must-implement-bcachefs, whose exit is the integrated writer's upstream fsck and every accepted geometry finishing its unmount.🤖 Generated with Claude Code