Repository navigation
A pager's objects are charged as they are made, and /tmp holds its files by that - #1273
Conversation
Review of #1273, round 1, at 47d8525The brief named 17884f5. The branch moved to 47d8525 during the review, so this covers both commits. ArchitectureThe ownership is right. The kernel makes object slots as they are needed and charges them to the pager's job. The file server decides how many objects its pager serves. A memory volume's files are now bounded by what the server's job can be charged, not by a sixteenth of its pages.
The no-holders walk. I was asked to decide whether it needed a structural fix. It did, and 47d8525 supplies one. At 17884f5 the walk ran under a ticket lock with preemption off, on every close of a Manual. No change is needed. The manual says nothing about how many objects a pager serves, and every allocation still has one owner. "Services … bound what clients can make them grow" is still unmet for The Size. Production grows by about 254 lines: Paths traced at 47d8525
BLOCKERS
SUGGESTIONS
Interactions with open PRs
Evidence checked
PR description (for the orchestrator)It is stale at 47d8525:
SEND BACK |
…he server keeps its own count The kernel's `objects` ceiling sized nothing once slots were made on demand: it was DATA's cache policy enforced in the kernel, and PAGER_MAX_OBJECTS existed only so a memory volume could ask for none. `pager_create` now takes a capacity, a dirty ceiling and a report time. A pager's objects are bounded by what its job can be charged and by the slot table's own limit, and either refusal answers ResourceExhausted. A disk volume's count moves into the file server: `Files::opened` lets its oldest file nobody holds go once it keeps its count, and refuses an open NoSpace while every one is held, as the kernel did. A memory volume keeps no count. The SDK, pagerprobe, a guest binary and the server's fake follow. `Slots::grow` reserves its piece before its list, so a list the heap refuses drops the piece and leaves the list's capacity as it was, which is what `make_slot`'s refund assumes (B1). The charge's field is `slots_charge` (S3). The /tmp per-client gap is its own record, issues/one-client-can-fill-the-tmp-servers-memory-and-deny-every-other-client.md, with the slots kept at a pager's peak, its measured 72 bytes, and an exit for each: the charge to the client, the owner's to decide, and a piece of free slots going back (B2, B3). It is out of four-shares, and no-physical-memory-fairness points at it. tmp_empty_files makes one file per MiB of the machine's memory and 1,024 more, so a run on any machine exceeds any count a share of its memory set (S4). virt_tmp_holds_empty_files_by_what_each_costs runs it on AArch64, in tests/images/virttmpcase.toml beside the memory file server alone (S5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
47d8525 to
d2b9c87
Compare
Review of #1273, round 2, at d2b9c87ArchitectureS1 makes the ownership cleaner. The kernel now bounds a pager's objects only by what its job can be charged and by its table's limit. DATA's cache count is a file-server policy, and it now lives in the file server. No object count remains in the kernel or the ABI:
Size against main.
Round-1 blockers
S1 at the server
S2 decline: correctS2's premise was
S3, S4
BLOCKERS
SUGGESTIONSNone. Evidence checkedMy runs used my own worktree and target directory on a macOS host, at d2b9c87. Every command exited 0.
Limits of these runs:
PR description (for the orchestrator)The cross-PR notes need two corrections:
The #1258 entry-size and list notes, and the #1269 lock-hold note, are accurate. SEND BACK |
A test runner given a job list opens `power` for its bound's reboot and exits before any job without it, so virt_tmp_holds_empty_files_by_what_each_costs could never see its job end. virttmpcase.toml's runner now receives it, as every other job list's does, and the comment says why. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he server keeps its own count The kernel's `objects` ceiling sized nothing once slots were made on demand: it was DATA's cache policy enforced in the kernel, and PAGER_MAX_OBJECTS existed only so a memory volume could ask for none. `pager_create` now takes a capacity, a dirty ceiling and a report time. A pager's objects are bounded by what its job can be charged and by the slot table's own limit, and either refusal answers ResourceExhausted. A disk volume's count moves into the file server: `Files::opened` lets its oldest file nobody holds go once it keeps its count, and refuses an open NoSpace while every one is held, as the kernel did. A memory volume keeps no count. The SDK, pagerprobe, a guest binary and the server's fake follow. `Slots::grow` reserves its piece before its list, so a list the heap refuses drops the piece and leaves the list's capacity as it was, which is what `make_slot`'s refund assumes (B1). The charge's field is `slots_charge` (S3). The /tmp per-client gap is its own record, issues/one-client-can-fill-the-tmp-servers-memory-and-deny-every-other-client.md, with the slots kept at a pager's peak, its measured 72 bytes, and an exit for each: the charge to the client, the owner's to decide, and a piece of free slots going back (B2, B3). It is out of four-shares, and no-physical-memory-fairness points at it. tmp_empty_files makes one file per MiB of the machine's memory and 1,024 more, so a run on any machine exceeds any count a share of its memory set (S4). virt_tmp_holds_empty_files_by_what_each_costs runs it on AArch64, in tests/images/virttmpcase.toml beside the memory file server alone (S5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A test runner given a job list opens `power` for its bound's reboot and exits before any job without it, so virt_tmp_holds_empty_files_by_what_each_costs could never see its job end. virttmpcase.toml's runner now receives it, as every other job list's does, and the comment says why. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
d9bc3e4 to
12aa41f
Compare
…les by that A pager made its whole object table when it was made: a slot of 60 bytes for every object it might serve, charged up front, and the file servers sized that count as #1099 sized the DATA cache, a sixteenth of the pages, between 1,024 and 65,536. DATA drops the oldest file nobody holds when the count is reached. A memory volume cannot: its anonymous objects are its files. So /tmp, and DATA when it falls back to memory, held at most that many files whatever they held: 3,468 on a 4 GiB guest, one file per 1.07 MiB of RAM, empty files included. The slots are now made a piece of 256 at a time (kernel::slots::Slots), and each piece is charged to the pager's job when an object first needs it. Only making an object allocates; a release never does, and a slot let go is the next one taken. The `objects` argument stays as the most a pager chooses to serve. A memory volume's pager asks for PAGER_MAX_OBJECTS (8,388,608), so its files are bounded by what the file server's job can be charged for them: 56 bytes of kernel heap and a few hundred of the server's for an empty file, and its pages. A disk volume keeps its cache count. The pieces are kept for the pager's life, so a volume keeps the slots its largest count of files needed. tmp_empty_files makes 10,000 empty files in /tmp and removes them; the tmp_holds_empty_files_by_what_each_costs row runs it on QEMU's 4 GiB x86-64 guest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… gap is recorded A pager found each object owed a no-holders notice by walking its slots from the first. That was bounded by 65,536 while the object count was, and now grows with the number of files a memory volume holds, under the pager's lock, on every notice a closed holder owes. The slot table now keeps its marked slots on a list through the slots themselves (Slots::mark, unmark, take_marked), so marking, unmarking and taking cost the same however many slots there are, and none allocates. A holder's going marks its object's slot; a release or a removal unmarks it; `notices` takes the marked slots one by one. The `no_holders` flag on each entry and the pager's count of them go. The one walk left is the pager's end, once. A slot is 72 bytes: its 56-byte entry and its place on the list. tmp_empty_files opens each file to read and closes it before removing it, so each of its 10,000 files' last holder is reported through the list. A memory volume's files stay charged to its server and bounded by memory. issues/four-shares-take-a-file-server.md records that one client can therefore fill the /tmp server's memory and deny the others, with the proposed exit, charging what a server keeps for a client to that client's job, pending the owner's decision; issues/no-physical-memory-fairness.md points at it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he server keeps its own count The kernel's `objects` ceiling sized nothing once slots were made on demand: it was DATA's cache policy enforced in the kernel, and PAGER_MAX_OBJECTS existed only so a memory volume could ask for none. `pager_create` now takes a capacity, a dirty ceiling and a report time. A pager's objects are bounded by what its job can be charged and by the slot table's own limit, and either refusal answers ResourceExhausted. A disk volume's count moves into the file server: `Files::opened` lets its oldest file nobody holds go once it keeps its count, and refuses an open NoSpace while every one is held, as the kernel did. A memory volume keeps no count. The SDK, pagerprobe, a guest binary and the server's fake follow. `Slots::grow` reserves its piece before its list, so a list the heap refuses drops the piece and leaves the list's capacity as it was, which is what `make_slot`'s refund assumes (B1). The charge's field is `slots_charge` (S3). The /tmp per-client gap is its own record, issues/one-client-can-fill-the-tmp-servers-memory-and-deny-every-other-client.md, with the slots kept at a pager's peak, its measured 72 bytes, and an exit for each: the charge to the client, the owner's to decide, and a piece of free slots going back (B2, B3). It is out of four-shares, and no-physical-memory-fairness points at it. tmp_empty_files makes one file per MiB of the machine's memory and 1,024 more, so a run on any machine exceeds any count a share of its memory set (S4). virt_tmp_holds_empty_files_by_what_each_costs runs it on AArch64, in tests/images/virttmpcase.toml beside the memory file server alone (S5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A test runner given a job list opens `power` for its bound's reboot and exits before any job without it, so virt_tmp_holds_empty_files_by_what_each_costs could never see its job end. virttmpcase.toml's runner now receives it, as every other job list's does, and the comment says why. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12aa41f to
3eaf817
Compare
|
Orchestrator: landing under the benches-down rule. Review r2's blocker (virttmpcase |
…to main's #1273 made /tmp hold its files by what each costs and left two follow-ups for this branch. `testcases_members_exit_0` judges `tmp_empty_files` on the same 4 GiB guest as the dedicated x86-64 row `tmp_holds_empty_files_by_what_each_costs` did, so that row and its function go. Its `DRIVEN_AND_SHARED` entry stays, now naming only the AArch64 row that still drives the binary. `issues/tmp-holds-one-file-per-megabyte-of-ram-whatever-the-files-hold.md` goes: #1273 lifted its count, and what is open of it, one client filling the server and denying the others, is `issues/one-client-can-fill-the-tmp-servers-memory-and-deny-every-other-client.md`. `readdir_bound`'s comment no longer counts its files against a pager's object count, which a memory volume no longer keeps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r's slots, as no-holders ones are Main brings #1273: a pager's object slots are made as they are wanted (kernel::slots::Slots), pager_create takes no object count, and the objects whose last holder went are a list through the slots, not a walk over every slot. #1258 had kept its dirtied objects the old way, a flag an entry carries, a count, and a walk in notices. - Slots holds a const number of mark lists, each apart from the others; a removal unmarks a slot on every one. The pager's table has two: NO_HOLDERS, #1273's, and DIRTIED. owe_dirtied marks and unmarks the second, has_notices asks both, notices takes from both, and a release unmarks both. The only walk over every slot left is the pager's end. - An entry keeps #1258's writers and record beside #1273's fields, and add makes it in the slot make_slot charged for. holder_gone counts the holder down and say_no_holders marks the object only once no holder's write is under way, as #1258 had it. - pager_create, the SDK's Pager::create and pagerprobe take no object count. - Busy came from both sides. #1261 on main made it general, an inbox's submitter among its uses, mapped to EBUSY and ResourceBusy; #1258 used it for a write to a file a program runs from, mapped to ETXTBSY and ExecutableFileBusy. It keeps main's mapping, and its doc names both uses: a holder's write to a sealed object now answers EBUSY. - The file server's tests take #1273's keeps argument (None where the count is not what a test is about) and #1258's record. - A slot grows from #1273's 72 bytes to 216: #1258's object state, record and writers, and the second mark. The /tmp record's per-file cost says so, measured as #1273 measured it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…record says who measures again B19. The `Mappers` doc pointed at `Object::epoch`, which r8 replaced with `Object::link`. `Pages::object` said a released object's id may be reused because `release` takes its pages, but `release` went in the composition with #1273. The docs now say a node is seen at the epoch `link` answers, and an id is reused once its object is empty: `invalidate` marks its pages and the walk's end takes them. B20. The /tmp record now says that whichever of #1269 and #1258 lands second measures the combined slot again, updates the figures and deletes the sentence. #1258 rewrites the same paragraph for its own slot growth. S1, S2: `Entry.refs` says only what it keeps: the slot, free with the last reference. It also counts mappings, walks, queued removals and writes, which the old list missed. The record says every entry carries the 64 bytes of mapping state, not only a mapped object's. Doc comments and a record only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…record says who measures again B19. The `Mappers` doc pointed at `Object::epoch`, which r8 replaced with `Object::link`. `Pages::object` said a released object's id may be reused because `release` takes its pages, but `release` went in the composition with #1273. The docs now say a node is seen at the epoch `link` answers, and an id is reused once its object is empty: `invalidate` marks its pages and the walk's end takes them. B20. The /tmp record now says that whichever of #1269 and #1258 lands second measures the combined slot again, updates the figures and deletes the sentence. #1258 rewrites the same paragraph for its own slot growth. S1, S2: `Entry.refs` says only what it keeps: the slot, free with the last reference. It also counts mappings, walks, queued removals and writes, which the old list missed. The record says every entry carries the 64 bytes of mapping state, not only a mapped object's. Doc comments and a record only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…record says who measures again B19. The `Mappers` doc pointed at `Object::epoch`, which r8 replaced with `Object::link`. `Pages::object` said a released object's id may be reused because `release` takes its pages, but `release` went in the composition with #1273. The docs now say a node is seen at the epoch `link` answers, and an id is reused once its object is empty: `invalidate` marks its pages and the walk's end takes them. B20. The /tmp record now says that whichever of #1269 and #1258 lands second measures the combined slot again, updates the figures and deletes the sentence. #1258 rewrites the same paragraph for its own slot growth. S1, S2: `Entry.refs` says only what it keeps: the slot, free with the last reference. It also counts mappings, walks, queued removals and writes, which the old list missed. The record says every entry carries the 64 bytes of mapping state, not only a mapped object's. Doc comments and a record only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
/tmpheld at most a fixed number of files whatever they held: 3,468 on a 4 GiB guest, one file per 1.07 MiB of RAM, empty files included (issues/tmp-holds-one-file-per-megabyte-of-ram-whatever-the-files-hold.mdon #1271; refusal at file 3,464 in its evidence).Where the number came from:
capacity / 16objects clamped to 1,024..65,536, which is 3,468 on 4 GiB. For DATA the count bounds a cache: past it, the server lets the oldest file nobody holds go. A memory volume's objects are its files, and nothing lets them go, so for/tmp, and for DATA in memory, the count was a hard cap.What changes
kernel::slots::Slots).pager_createtakes no object count. A pager's objects are bounded by what its job can be charged and by the slot table's own limit, and either refusal answersResourceExhausted.Files::openedlets the oldest file nobody holds go once a disk volume keeps its count (DATA one per 16 pages, 1,024 to 65,536; LOG and BOOT 1,024), and refusesNoSpacewhile every one is held, as the kernel did. A memory volume keeps no count, so/tmpis bounded by what its server can be charged.noticestakes the marked slots.no_holdersflag and its count are gone. The only walk over every slot left is the pager's end, which happens once./tmpfile costs one 72-byte kernel slot (a 56-byte entry and a 12-byte mark), a few hundred bytes of the server's heap, and no page.No manual page changes: the manual says nothing about how many objects a pager serves, and nothing in the manual names the ceiling.
Who is charged, and what bounds one client
Everything a
/tmpfile costs is still the memory file server's: its slot, the server's bookkeeping and its pages.issues/one-client-can-fill-the-tmp-servers-memory-and-deny-every-other-client.md(severity: [security]). It holds:/tmp's server and denying the others;issues/no-physical-memory-fairness.mdpoints at that record.For whichever lands second
/tmprecord, or reduce it to a pointer: this branch meets its count, and its charge to the maker lives in the record above.tmp_holds_empty_files_by_what_each_costs; The shared boots run as guest rows, each member judged by its own exit #1271'stestcases_members_exit_0judgestmp_empty_fileson the same 4 GiB guest.DRIVEN_AND_SHAREDentry, with its comment naming only the AArch64 row.virt_tmp_holds_empty_files_by_what_each_costsstill namestest_rs_tmp_empty_files, so removing the entry redssuite_split.dirtiedflag must come off a list too, either a second one inSlotsor one "owes a notice" list with a kind per entry, withhas_noticesfollowing, so that no walk over all slots returns.recordand its other new fields take a slot from 72 bytes to about 144.create,add,holder_gone(intosay_no_holders),unref_locked,notices, and the ABI nearpager_create, whoseobjectsargument is gone here.tests/guest/pagerprobe/src/main.rs:59there,Pager::create(LARGE + 16 + WRITES, 1, 5, …), still passes an object count; it must lose it. That caller is guest-built, so no host check finds it.add,let_go,removing,unref_lockedandon_zero_handles.let_godrops the pager lock for revoke walks;entries.unmark(slot)must stay in the same hold asowned = false.tests/guest/mapprobe/src/main.rs:60there,Pager::create(CAPACITY, 0, 8, REPORT), still passes an object count; it must lose it. That caller is guest-built, so no host check finds it.Elsewhere the conflicts are the slot table's accessors:
entries[slot as usize].as_mut()becomesentries.get_mut(slot), and anEntrygoes in throughinsert_with.Review round 1
growreserves the piece first.pagerprobe,a_served_file_reads_through_its_paged_objectand the server's fake.grown_bytes()returningNoneis a pager holding as many objects as the table holds. That is reachable from userland, somake_slotanswersResourceExhaustedrather than panicking.slots_charge.tmp_empty_filesmakes one file per MiB of the machine's memory and 1,024 more.virt_tmp_holds_empty_files_by_what_each_costsruns it on AArch64, intests/images/virttmpcase.tomlbeside the memory file server alone.Slots::heldwent with the count, its only caller.Review round 2
The AArch64 case's test runner now receives
power. A runner given a job list opens it for its bound's reboot, and exits before any job without it.Checks
The host is macOS arm64. Logs are at evidence pr-1273, in
r2/tor5/for the rounds.At 3eaf817, rebased onto main ce202b2 (#1262):
tests/toyos.rs; all three are kept.git range-diffshows every commit unchanged but for that hunk's anchor, and the branch's whole diff against its base is the same before and after.cargo test --test toyos-checksand the harness's--list: exit 0.At 12aa41f, rebased onto main 75bdec3 (#1259), every command exit 0:
cargo test -p kernel --lib: 486 passed.cargo test --test toyos-checks.cargo run -- --clippy: 26 invocations clean.cargo run -- --build-only.How the rebase went:
virt_vm_map_sharesrow and this branch's AArch64 row were added at the same place; both are kept.Pager::createcaller on the rebased tree passes three arguments.mapprobeis A served file is mapped read-only and shared, and a fault on its absent page waits for its pager #1269's, not main's (see above).At d9bc3e4 (round 3, which changes only
tests/images/virttmpcase.toml):cargo test --lib -- build::tests: exit 0, 48 passed. These are the config gates over the changed config.At d2b9c87 (round 2, rebased onto main 3b93f37):
cargo test --test toyos-checks: exit 0.cargo run -- --build-only: exit 0.On the same change before that rebase (07ea739c3), every command exit 0:
cargo test -p kernel --lib,-p toyos-abiand-p toyos.a_disk_volume_keeps_its_count_and_lets_its_oldest_closed_file_go.virttmpcase.toml.cargo test --test toyos-checks.cargo run -- --clippy: 26 invocations clean.Negative controls, each a checked patch, run and restored in the same session:
unmarkleaves the next slot's back-link staleFiles::openednever applies its countRound 1's
cargo run -- --ci hostwas 113 of 114 steps green. Its one red is the macOS-onlyllvmenvironment test (issues/the-llvm-environment-test-breaks-git-on-macos.md); nothing underbuild/changed but the config list.Guest: run 43123313540 at 12aa41f, green on both architectures: x86-64 made 5,068 empty files, past the old cap of 3,468, and AArch64 made 3,030. It stands for 3eaf817, whose change from 12aa41f is #1262's rows alone. Its rows:
tmp_holds_empty_files_by_what_each_costson x86-64's 4 GiB guest;virt_tmp_holds_empty_files_by_what_each_costson AArch64's;a_spawn_from_an_image_frees_it_when_the_child_ends: objects on DATA and/tmp, and the census;a_row_reaches_its_own_state_and_no_other: DATA's objects through the server's own count.Earlier runs 43110990766, 43115186545, 43118861674 and 43121026670 were cancelled before they ran.
Control. On the base, the same 4 GiB x86-64 guest refused the 3,465th empty file in
/tmp(#1271's evidence), byreaddir_boundon the same create path.Owed on metal: nothing. The claim is a count, with no timing or hardware in it.
🤖 Generated with Claude Code