Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 18 additions & 8 deletions issues/a-file-held-across-a-file-servers-restart-answers-gone.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,22 @@ opened: 2026-09-27

# A file held across a file server's restart answers Gone

No volume records an object id, so nothing tells the held file from another
put at its path since, and a file held across a restart answers `Gone`
(`toyos::files`, so std's `File` and a C descriptor alike); `logd` loses
`/log` for the boot if LOG's server restarts.
A file id is good only on the connection it was answered on, and a server's
volume id is its pid (`system/fileserver/src/main.rs`), so a file held across
a restart answers `Stale` or `Gone` (`toyos::fs`, so std's `File` and a C
descriptor alike), and a program that does not open its path again loses
what it writes after. logkeeper does: a part its volume refuses or loses is
opened again and given the rest from its replay
(`system/logkeeper/src/store.rs`). Reopening by path is the hazard: a stick
swapped for another carrying a file under the same name has that file
rewritten past what this boot last synced to it.

**Exit**: DATA's entry carries an object id and a generation that nothing
reuses, a held handle reopens only when both match, and a host test that
makes a same-length file in a held file's freed block within the second has
the reopen refused.
On DATA a held file can be told from another put at its path since: bcachefs
never reuses an inode number (`next_inode`, A8.9). Reopening it by that
number waits on a volume identity that outlives a restart, which the pid is
not; ruled so by the coordinator, 2026-11-05. A FAT role has no such
number and keeps `Gone`.

**Exit**: a file held on DATA across its server's restart is reopened by its
volume's identity and its inode number, and a host test that makes a file at
a held file's path before the restart has the held one still name its own.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
status: open
kind: finding
opened: 2026-11-10
severity: [data-loss]
---

# A log volume full of logkeeper's own parts is never written again

`logkeeper` deletes its old files in one place, `sweep`
(`system/logkeeper/src/store.rs`), which runs from `Volume::open`, before the
boot's first create, and from `Volume::rotate`. It keeps at most
`MAX_LOG_FILES` files and counts files, not bytes. A part the volume refused
is opened again by `Volume::reopen`, which does not sweep, and a refused part
never fills, so it never reaches a rotation.

So where `/log` has no room and holds fewer than `MAX_LOG_FILES` of
`logkeeper`'s files, nothing frees any: the open's sweep deletes nothing, the
create or a later write is refused, and every retry is refused the same way
for the rest of the boot. A later boot whose first create is refused adds no
file, so it stands where this one did. The boot's log is then in the replay
and on the console only, which `logkeeper` says, and the stop is not marked.

With `MAX_LOG_FILES` or more files the open's sweep deletes only as many as
the count is over by, one where there are sixteen, and a rotation one more.
That frees what the retired files held and no more, so on a volume otherwise
full the boot is refused once it has written past that, and from there stands
as above.

Sixteen parts of `MAX_LOG_BYTES` are under half of the smallest log volume, so
this needs something else to have filled it: the bootloader's files, a
diagnostic dropped beside the logs, or a volume smaller than the build makes.

Read from the code. The refusal and the doubling retry were seen on the guest
runner on a volume a filler file had filled (run 42133377735's stalled row);
no run has filled a volume with `logkeeper`'s own parts.

**Exit**: a part refused for want of room frees room from `logkeeper`'s own
older files by `retire`'s order before it is asked again, or the record of a
ruling that a full log volume is the owner's to clear.

Owner: `system/logkeeper`; unheld.
9 changes: 6 additions & 3 deletions issues/a-machine-without-an-iommu-refuses-every-claim.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@ On such a machine whose image is on its NVMe disk, which
it: `diskserver` is refused the controller (`diskserver: NOT SERVING —
pci:1b36:0010 is on this machine and the kernel refused this service its
claim`), so the three file servers say `Data serving … — absent`, `Log serving
/log — absent` and `Boot serving /boot — absent`, and `logkeeper` says `no
/log on this machine - this boot's kernel log is on the console only`. Read off the no-unit arm of `iommu_virtio_platform`, which is
green with it: the arm asserts the refusal and reads no volume. **The kernel
/log — absent` and `Boot serving /boot — absent`, and `logkeeper` says its
first part `is asked again - this boot's lines are kept until /log takes
them`, publishes `degraded`, and asks at a doubling wait to 64 s for the rest
of the boot. The file servers' lines are read off the no-unit arm of
`iommu_virtio_platform`, which is green with them; `logkeeper`'s is read from
its code, since the arm asserts the refusal and reads no volume. **The kernel
drives no USB either** since usbd took the xHCI (the small-kernel track's step
10), so on such a machine usbd is refused its controller too, a USB stick's
volumes are absent like the NVMe disk's, and no USB keyboard or pointer
Expand Down

This file was deleted.

3 changes: 2 additions & 1 deletion issues/std-file-lock-answers-ok-and-locks-nothing.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ cargo is one such program. Every file lock it takes is one of these calls
(its `src/util/flock.rs` at `7c83d4cc0953`, the cargo the fork pins), so two
cargo processes in one ToyOS are kept apart by nothing.

Owner: `issues/toyos-builds-itself.md`, whose M4 runs cargo in the guest;
Owner: `issues/toyos-builds-itself.md`, whose M4 runs cargo in the guest
(where it stays, by the coordinator's ruling of 2026-11-05);
the locks libc owes for the same milestone are
`issues/libc-refuses-what-toyos-cannot-yet-answer.md`'s.

Expand Down
9 changes: 9 additions & 0 deletions issues/storage-is-layers-and-a-role-is-a-filesystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ what is left here is built on them.
and ROOT), and a link that fails is refused by name.
7. NTFS read-only, postponed here by the owner.

**A DATA commit's durability costs two Flushes per journal entry**, three for
an entry naming itself (`write_entries`, `lib/bcachefs/src/engine/mount/mod.rs`):
a flush, the entry, a flush. A write with Force Unit Access is durable when
it completes, with no Flush after it (NVM Express NVM Command Set, Write's
FUA bit; SCSI SBC, WRITE(10)'s FUA bit). FUA is missing end to end: `toyos-blockring`'s
Write carries no flag, the nvme driver never sets Write's FUA bit, usbd
never sets WRITE(10)'s, and bcachefs asks for none. Assigned with #1131,
whose APST wake cost the second Flush is part of.

**A file's pages are the kernel's paged objects'** (memory stage 7a, built by
this track): one per file, supplied by its file server, read by its holders
without a request (`system/fileserver/src/pages.rs`). The server's block cache
Expand Down
31 changes: 21 additions & 10 deletions issues/the-console-waits-on-logkeepers-file-writes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,22 @@ severity: [hang]
# The console waits on logkeeper's file writes

`logkeeper`'s loop is the only thing that reads the rings and grows the
replay the console's carriers and readers are fed from, and it writes and
syncs each round to `/log` itself (`Log::to_volume`, `Volume::write` and
`Volume::sync` in `system/logkeeper/src`), each a blocking call on the LOG
file server. `LOG_WRITE_BUDGET` judges a call only once it returns. So a LOG
server that takes a write and never answers, alive but wedged, leaves every
console channel silent for as long as it does, with no failure bound, and the
supervisor restarts only a server that ended.
replay the console's carriers and readers are fed from, and it makes every
call on the LOG file server itself, each one blocking (`system/logkeeper/src`):

- a round's write and sync (`Log::to_volume`: `Volume::write`, `Volume::sync`);
- the catch-up of a file behind the replay (`Log::caught_up`, from a round and
from `Log::adopt`: `Volume::catch_up`'s reopen, writes, sync and rotation);
- a rotation's sync, retention sweep and create (`Log::rotate_if_full`);
- the stop's catch-up and sync (`Volume::finish`), whose time bounds how many
calls it starts and not how long one takes;
- the stop mark and its removal (`store::mark_stopped`, `unmark_stopped`).

`LOG_WRITE_BUDGET` judges a call only once it returns. So a LOG server that
takes a call and never answers, alive but wedged, leaves every console channel
silent for as long as it does, with no failure bound, and the supervisor
restarts only a server that ended. The stop's wait on the volume's opener is
the one wait here with a bound (`STOP_CATCH_UP`).

Read from the code, not reproduced. The volume's open waited the same way
before it moved to a thread of its own, which is how the console stood silent
Expand All @@ -24,8 +33,10 @@ for the supervisor's 30 s declaration bound on `acpi_press_is_one_sci`'s boot.
server stops answering mid-boot still carries its later lines. The shape: the
file is a consumer of the replay with its own offset, written on its own
thread as the readers and carriers are, and the stop asks it to reach the
replay's end and sync within a bound. #1190's per-file offset and bounded
stop catch-up (`Volume::through`, `catch_up`, `finish`) are its vehicle, so
whoever reconciles #1190 with this takes it up.
replay's end and sync within a bound. The per-file offset and the stop's
bounded catch-up are in the tree (`Volume::through`, `catch_up`, `finish`).
What is left is the move: the loop still owns the `Volume` and makes the calls
above, so the volume goes to a thread of its own that reads the replay, and
the stop's bound becomes a wait on that thread, which the loop can give up on.

Owner: `system/logkeeper`; unheld.
3 changes: 2 additions & 1 deletion lib/logstream/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ pub const REGISTER: u32 = 1;
pub const SERVED: u32 = 2;

/// The supervisor → `logkeeper`: the machine is about to be stopped. `logkeeper` reads every ring
/// and the kernel's records, writes them, makes the volume durable, and
/// and the kernel's records, writes them, makes the volume durable — or says,
/// in the file and on the console, what of the boot it could not — and
/// answers [`FLUSHED`]; the supervisor asks for the stop once it has, or once its bound
/// is spent.
pub const FLUSH: u32 = 5;
Expand Down
6 changes: 6 additions & 0 deletions system/logkeeper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ toyos-logstream = { path = "../../lib/logstream" }
toyos-elide = { path = "../../lib/elide" }
toyos-inspect = { path = "../../lib/inspect" }
toyos-wallclock = { path = "../../lib/wallclock" }
# The supervisor's bound on the stop's flush, which the stop's catch-up keeps inside.
toyos-quiesce = { path = "../../lib/quiesce" }

[dev-dependencies]
# A scratch `/log` for the volume's own tests.
toyos-tmpdir = { path = "../../lib/tmpdir" }

[package.metadata.toyos.host]
exempt.owns = "the kernel's record ring, read under the `logread` SysCap"
3 changes: 2 additions & 1 deletion system/logkeeper/src/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ const ACCEPT: u64 = u64::MAX;
pub enum State {
/// Every round written, flushed and published.
Writing = 0,
/// Every round answered, and slower than a log is worth.
/// The volume answers slower than a log is worth, or refused the part
/// and is asked again: the replay keeps what the file is owed.
Degraded = 1,
/// No volume, or one given up on: the log is on the console only.
ConsoleOnly = 2,
Expand Down
Loading