Skip to content

A file's mtime is wall-clock time: nanoseconds since the Unix epoch, UTC - #587

Merged
Japabu merged 10 commits into
mainfrom
wt/toyos-mtime
Sep 29, 2026
Merged

Japabu merged 10 commits into
mainfrom
wt/toyos-mtime

Conversation

@Japabu

@Japabu Japabu commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

The owner approved this ABI change for the self-hosting track. ToyOS will build LLVM on itself, and ninja and make decide what to rebuild by mtime. So a file's modification time is now wall-clock time, in nanoseconds, and a reboot does not change it.

The contract

toyos_abi::syscall::Stat::mtime is nanoseconds since the Unix epoch, UTC. It is taken off the wall clock at the write, to the resolution its mount keeps:

mount resolution
DATA (bcachefs) and /tmp (tmpfs) the nanosecond: the u64 is stored as given
FAT (/boot, /log) two seconds, UTC, taken at the flush, read back as UTC nanoseconds of the even second
ROOT undated: src/image.rs stamps 0, for a reproducible image

The source is #583's UTC anchor. Since #583 the RTC keeps UTC and clock holds a single anchor, BOOT_SECS. utc_nanos is BOOT_SECS·10⁹ + nanos_since_boot(). SYS_CLOCK_EPOCH is its whole seconds, and a write stamps it, so a stamp and the epoch syscall cannot disagree about a second.

0 is undated. A machine whose RTC never answered has no date, so a file written there is stamped 0, never 1970 plus uptime. std's Metadata::modified answers 0 with ErrorKind::Unsupported. That is the kind SYS_CLOCK_EPOCH's refusal maps to on that machine, where SystemTime::now panics. libc reports 0 as 0.

Accuracy is the RTC's second, and resolution is the counter's. The anchor truncates the RTC's second, as SYS_CLOCK_EPOCH already did.

The layout of Stat is unchanged (one u64), so no syscall changes. The sysroot key moves, because libc's struct stat and the std fork change.

What changed, per decision

  • kernel/src/clock.rs
    • utc_nanos is the anchor carried on by the counter.
    • utc_secs (SYS_CLOCK_EPOCH) is utc_nanos / 10⁹. That is the value main's utc_secs computed, since ⌊(B·10⁹ + n)/10⁹⌋ = B + ⌊n/10⁹⌋.
    • mtime_now is what a write stamps: utc_nanos, or 0 with no RTC.
    • NANOS_PER_SEC is pub, for the FAT adapter.
  • object/ops.rs, vfs.rs, revoke_selftest.rs, leak_selftest.rs stamp with mtime_now wherever they stamped nanos_since_boot. The sites are: create with truncation, create of a missing file, write, ftruncate, and the flush open_backing_identified makes.
  • fat32_adapter.rs
    • stamp(mtime) is the whole UTC seconds of the mtime. FAT specifies local time, and this stamps UTC because the owner ruled the hardware clock is UTC.
    • create stamps the mtime the VFS hands it. Before, it stamped now() and ignored the argument.
    • update_metadata stamps the flush's own instant, as on main, and ignores the flushing handle's mtime: the last handle to close can be a reader that opened before the last write, and its mtime would store the file as older than its contents.
    • file_mtime answers modified_unix·10⁹, the same unit as every other mount.
    • now() is stamp(mtime_now()), for the flush, reconcile and mkdir. An undated 0 clamps to FatTime::EPOCH, which is what main's now() gave with no RTC.
  • std: Metadata::modified answers Unsupported for 0. Otherwise it already read the word as nanoseconds since UNIX_EPOCH, so it is correct now and was wrong before. The change is fork commit ac63a08077f on ToyOSOrg/rust branch wt-toyos-mtime. It is pinned as 90697f1401a, which merges main's pin 9c3eea441d8 (toolchain: LLVM, with clang and LLD, is built once per key and every compiler links it #597) with no conflicts.
  • libc
    • struct stat gets POSIX's st_atim/st_mtim/st_ctim (struct timespec), and st_mtime becomes the macro POSIX defines. fstat used to put nanoseconds into st_mtime, which is seconds.
    • st_nlink is u64, as nlink_t (unsigned long) is. The Rust Stat was 112 bytes against C's 120. A C caller read st_blksize (0) as st_size and the stamp's tv_nsec as st_mtime, and fstat left C's last 8 bytes unwritten. That mismatch predates this branch; nothing in the tree called stat from C.

Tests

  • 202_stat_mtime (C, Fast): writes 17 bytes to /tmp, then fstats and stats the file. It prints sizeof(struct stat) (120), st_size (17), whether st_mtime lies between two time(NULL) readings around the write, and whether st_mtim.tv_nsec is below a second.

  • file_mtime (Rust, Fast, shared boot) judges /tmp. Each stamp must lie between two SYS_CLOCK_EPOCH readings taken around what made it: before·10⁹ ≤ mtime < (after+1)·10⁹. It covers four stamp sites:

    • two writes, the second strictly later than the first, which a clock of whole seconds cannot show;
    • File::create with no write (create with truncation);
    • OpenOptions::new().write(true).create(true) on a path first shown to be missing (create of a missing file);
    • a reopen of the first without truncation, then set_len(1) and sync_all (ftruncate), which must stamp strictly later than its create.

    Then /log (FAT): it writes, closes and reopens /log/file-mtime, and requires (before−1)·10⁹ ≤ stamp ≤ after·10⁹ and a whole second, with after read after the reopen's stat.

  • file_mtime_undated (Nightly): boots with rtc-dead and checks that the kernel refused the clock by name. It then runs file_mtime undated, which writes /tmp/file-mtime-undated without asking the time and requires modified() to fail with Unsupported.

  • file_mtime_survives_a_reboot (Nightly), two boots of one DATA image:

    1. Boot 1 stages the RTC at 2033-03-07T09:14:25 with -rtc base= and writes /home/file-mtime.bin. The printed stamp must lie within after_the_base of that instant.
    2. With the guest gone, the host reads the same stamp off the image with its own bcachefs reader.
    3. Boot 2 has the RTC a day on and must read the stamp back unchanged, so a re-stamp at mount or open would show.
  • toyos-fat32 a_write_time_keeps_the_even_second (host) tests the conversion behind the FAT row: a write time drops the odd second that FatTime keeps only for creation times.

  • main's wall_clock_utc checks the FAT stamp of the log against the staged RTC. The FAT stamp is now stamp(mtime_now()), so that test also covers this branch's FAT path.

Gates

At f7caded3, the merge of origin/main at 4de5ecdb. Host only: I ran no QEMU.

command exit
cargo run -- --ci host 0 (54 steps, all green)
cargo run -- --build-only 0
cargo test --test toyos-build -- --list 0 (lists Fast 202_stat_mtime, Fast file_mtime, Nightly file_mtime_survives_a_reboot, Nightly file_mtime_undated)

High-risk: ABI and filesystems

  • Mutations. A script applied each one, built it with cargo run -- --build-only, reverted it, and showed the tree clean afterwards. Each must turn its test red:
    • nlink-u32.patch (libc back to the pre-branch st_nlink: u32) → 202_stat_mtime: st_size reads 0 and st_mtime reads a nanosecond count.
    • site-114.patch (create with truncation back to nanos_since_boot()) → file_mtime.
    • undated.patch (unwrap_or(0) → unwrap_or_else(nanos_since_boot)) → file_mtime_undated: modified() answers 1970 plus uptime.
    • m132-ops, m821-ops (create of a missing file, and ftruncate, back to nanos_since_boot()) → file_mtime.
    • m926-fat-seconds (the FAT adapter's file_mtime answers seconds unconverted) → file_mtime's /log arm. These three are in the PR comment of round 5; each built at f7caded3 (exit 0).
  • Negative control. git diff HEAD origin/main over kernel/src, toyos-abi/src, userland/libc, tests/testcases/LICENSE and 202_stat_mtime.{c,expect}. That reverts the whole implementation onto 7e151819 and keeps every other test. 202_stat_mtime is dropped because main's header has no st_mtim, so the test cannot compile there. The std pin is kept, since it only changes what 0 means. At 389f572c the script applied it (12 files), built it (EXIT=0), reverted it and showed the tree clean.
    • file_mtime goes red on its first assertion, since a stamp since boot is about 10⁹·uptime, far below before·10⁹.
    • file_mtime_survives_a_reboot goes red on the oracle: a drift of about −1.99·10⁹ s.
  • Independent oracle. The instant the host stages in the emulated RTC with -rtc base=, the same oracle the wall_clock_* tests use. For the C layout, clang's own sizeof/offsetof over the header.

What I am unsure of

  • An undated machine stamps every file 0, so the shared-object cache's BackingId (size plus mtime) no longer sees a same-size rewrite on any mount there. Before, nanos_since_boot kept two writes apart. Recorded in the same-size-rewrite section of issues/isolation/the-so-caches-refusals-are-narrower-than-its-reach.md.
  • FAT cannot store undated. FatTime::from_unix_secs clamps 0 to 1980-01-01, which reads back as a date. Filed: issues/filesystem/an-undated-file-on-fat-reads-back-as-1980.md.
  • std's modified() refuses a directory and a symlink. stat of a directory and lstat of a symlink answer mtime 0, which std now reports as Unsupported where it answered Ok before. Recorded in issues/filesystem/std-calls-undated-what-it-did-not-stat.md.
  • Userland reads the wall clock in whole seconds. std's SystemTime::now and libc's clock_gettime do, so a just-written file can read as up to a second in the future against the program's own "now". Filed: issues/filesystem/userlands-wall-clock-has-whole-seconds-and-an-mtime-has-nanoseconds.md.

Closes issues/filesystem/a-files-mtime-is-nanoseconds-since-boot.md. Also filed, found by the audit and not fixed here:

  • issues/filesystem/the-last-handle-to-close-stamps-the-file-with-its-own-mtime.md
  • issues/filesystem/a-fat-files-mtime-reads-finer-through-its-writer-than-after-a-reopen.md
  • issues/filesystem/std-calls-undated-what-it-did-not-stat.md: DirEntry::metadata answers 0 for a file the kernel has a stamp for, a directory's stat and a symlink's lstat answer 0, and set_times returns Ok having set nothing.

🤖 Generated with Claude Code

https://claude.ai/code/session_01U6SVYFkdvV2t38KzNrESxs

Japabu and others added 2 commits September 28, 2026 21:22
Stat::mtime is documented and stamped as nanoseconds since boot, DATA
stores that number across reboots, the FAT adapter answers local Unix
seconds instead, and std and libc read both as time since the epoch. A
build tool comparing mtimes is misled by every reboot. Filed so the fix
that follows closes it by name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner approved this ABI change for the self-hosting track: ninja and
make decide rebuilds by mtime, and an mtime since boot made every
reboot reorder the tree.

Stat::mtime is now nanoseconds since the Unix epoch in UTC, off the wall
clock at the write, and never a time since boot. The layout is
unchanged (a u64); the meaning is the kernel's to keep:

- kernel/src/clock.rs: utc_nanos is the RTC's whole-second anchor
  carried on by the counter, and utc_secs (SYS_CLOCK_EPOCH) is now its
  whole seconds, so a stamp taken after the epoch syscall answered s is
  at least s seconds - the two can never disagree about a second.
  mtime_now is what a write stamps: utc_nanos, or on a machine whose RTC
  never answered, a clock that starts at the epoch at boot, so one
  boot's stamps still order. local_of_utc and utc_of_local convert for
  a format that stores local time.
- object/ops.rs, vfs.rs and the two self-tests stamp with mtime_now
  where they stamped nanos_since_boot.
- fat32_adapter.rs stamps the mtime the VFS hands it (the write's
  instant, not the flush's) as local seconds, and answers file_mtime as
  UTC nanoseconds; it used to answer local seconds, a different unit
  from every other mount. FAT keeps two seconds of local time, which
  the ABI doc now says, and toyos-fat32 gains a host test that a write
  time drops the odd second.
- bcachefs and tmpfs store the u64 as given, so DATA keeps nanoseconds
  across a reboot; ROOT's image stamps 0, as src/image.rs always did.
- libc's struct stat gains POSIX's st_atim/st_mtim/st_ctim, with
  st_mtime as the macro POSIX defines; fstat put nanoseconds into
  st_mtime, which is seconds.
- std needs no change: the fork's Metadata::modified already reads the
  word as nanoseconds since UNIX_EPOCH. sshd's SFTP attributes, which
  read modified(), now report real times.

The contract is the one PR #536's fsd already meets: it reports
nanoseconds since the epoch, off SYS_CLOCK_EPOCH's whole seconds.

Tests: file_mtime judges /tmp's stamp between two SYS_CLOCK_EPOCH
readings and requires a second write's stamp to be later (whole
seconds cannot say that), on the shared boot; its write/read modes are
driven by file_mtime_survives_a_reboot, which stages the RTC with
-rtc base=, checks DATA's stamp against that instant, reads the same
stamp off the image with the host's bcachefs reader, and boots again
with the RTC a day on to read it back unchanged.

Closes issues/filesystem/a-files-mtime-is-nanoseconds-since-boot.md.
Files what the audit found and this does not fix:
userlands-wall-clock-has-whole-seconds-and-an-mtime-has-nanoseconds,
the-last-handle-to-close-stamps-the-file-with-its-own-mtime,
a-fat-files-mtime-reads-finer-through-its-writer-than-after-a-reopen,
std-answers-an-mtime-of-1970-for-what-it-did-not-stat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu marked this pull request as ready for review September 28, 2026 19:36
@Japabu

Japabu commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 1, at 32948da

Ready for review. CI host concluded success on 32948da (run 36473365989; the earlier run 36472224038 was skipped). The orchestrator's guest runs at 32948da exited 0: file_mtime, file_mtime_survives_a_reboot, kernel_log_file, wall_clock_zone and Fast. The whole-change control exited 1 on both new tests, for the named reason. git merge-tree --write-tree origin/main 32948daf is clean. Net size: +396/−27. Production is +80/−27 (kernel +57/−20, toyos-abi +6/−1, libc +18/−7), tests +225, issues +91.

BLOCKER

  • userland/libc/src/posix_io.rs:488: st_nlink: u32 does not match nlink_t = unsigned long (include/sys/types.h:14). The Rust Stat is 112 bytes with st_mtim at offset 80, and C's struct stat is 120 bytes with st_mtim at 88. So a C caller's st_mtime reads the stamp's tv_nsec, and its st_size (offset 48) reads st_blksize, which is 0. fstat also leaves C's last 8 bytes unwritten. The PR's libc claim is false, and no C source in the tree calls stat or fstat, so nothing measures it. Fix: st_nlink: u64. Add a C test to tests/testcases that writes N bytes, then fstats and stats the file, and asserts sizeof(struct stat) == 120, st_size == N, before <= st_mtime <= time(NULL) and 0 <= st_mtim.tv_nsec < 1000000000. It must be red at 32948da.
  • kernel/src/object/ops.rs:114,132,821: three of the four stamp sites the PR names can each go back to crate::clock::nanos_since_boot() and every test stays green. The sites are create with truncate, create of a missing file, and ftruncate. file_mtime always writes after it creates, so only :526 is observed, and nothing calls ftruncate. Apply each patch alone: replace crate::clock::mtime_now() with crate::clock::nanos_since_boot() at :114, at :132 and at :821. Tests that must turn them red, in file_mtime's shared arm, each checked against write_judged's window:
  • kernel/src/clock.rs:202: the contract's no-RTC clause, which the ABI doc repeats, has no test. unwrap_or_else(nanos_since_boot) → unwrap_or(0) passes every run. The test: a file_mtime mode that never calls SYS_CLOCK_EPOCH, run on the rtc-dead machine (wall_clock_rtc_dead's params). It writes /tmp twice and asserts 0 < first < second < 86_400·10⁹. It must be red under the patch.

NOTE

REMOVE

SEND BACK

Japabu and others added 2 commits September 28, 2026 23:30
…every stamp site observed

- clock: `mtime_now` is `utc_nanos().unwrap_or(0)`. A machine whose RTC
  never answered has no date, so a file written there is undated (0),
  never 1970 plus uptime. `NANOS_PER_SEC` is private and used for every
  second in the module; FAT's two conversions take and give an `mtime`.
- toyos-abi: `Stat::mtime` states each mount's resolution and that 0 is
  undated; the path citations and "never a time since boot" are gone.
- std (fork ac63a08077f, `wt-toyos-mtime`): `Metadata::modified` answers
  `Unsupported` for 0, the kind `SYS_CLOCK_EPOCH` refuses with there.
- libc: `st_nlink` is `u64`, as `nlink_t` is. The Rust `Stat` was 112
  bytes against C's 120, so C read `st_blksize` as `st_size` and a
  stamp's nanoseconds as `st_mtime`. `202_stat_mtime` asserts the size,
  the length, the seconds and the nanoseconds through `fstat` and `stat`.
- `file_mtime` judges a create with truncation, a create of a missing
  file and a truncation against the wall clock, and its `undated` mode,
  run by the new `file_mtime_undated` on the `rtc-dead` machine, finds a
  written file undated without ever asking the time.
- Issues: the last-handle defect now reaches FAT; the std issue is renamed
  to what stays true; filed: an undated FAT file reads back as 1980, and
  the shared-object cache's identity is blind on an undated machine. The
  so-cache issue's `nanos_since_boot` claim is deleted.

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

Japabu commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2, head 57cc41e: gh pr view 587 --json mergeable returns CONFLICTING against origin/main 0368861. git merge-tree --write-tree origin/main 57cc41e4 exits 1 with a content conflict in tests/testcases/LICENSE. host passes on 57cc41e, but it is not measured against the tree this branch would merge into.

NOT READY FOR REVIEW

Japabu and others added 2 commits September 29, 2026 08:25
Recount tests/testcases/LICENSE's tinycc totals from the tree as merged
(318 files, 62 not upstream, 61 written here) instead of either side's
stale count. main's wall-clock boot-drift refactor (MAX_BOOT_DRIFT_SECS
replaced by after_the_base, measured against the boot's own elapsed
time) merged without a textual conflict but left file_mtime_survives_a_reboot
referencing the deleted constant; it now uses after_the_base like main's
other callers, with mtime_boot returning the elapsed Duration alongside
the printed mtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main's #583 made the RTC UTC: `clock` keeps one anchor, BOOT_SECS, and
the firmware zone, UTC_OFFSET_SECS and local_secs are gone. The mtime
now comes from that same anchor.

- kernel/src/clock.rs: main's `init_wall(century_reg)`, BOOT_SECS and
  its "reads {civil} UTC" line are kept. This branch's utc_nanos is
  BOOT_SECS * 10^9 plus nanos_since_boot, and utc_secs stays its whole
  seconds, which is the value main's utc_secs computed. mtime_now stays
  utc_nanos, or 0 (undated) when the RTC never answered.
  local_secs_of and mtime_of_local are deleted because no zone is left
  to convert through. NANOS_PER_SEC becomes pub for the FAT adapter.
- kernel/src/fat32_adapter.rs: this branch's `stamp(mtime)` and
  `now() = stamp(mtime_now())` are kept, now as whole UTC seconds of
  the mtime. main's refusal-reason moves onto `stamp`: FAT specifies
  local time, and this stamps UTC because the owner ruled the hardware
  clock is UTC. `file_mtime` answers modified_unix * 10^9. An undated
  0 clamps to FatTime::EPOCH, which is what main's now() gave with no
  RTC.
- tests/toyos.rs: main's wall_clock_utc row, CARRIES entry and dispatch
  arm replace wall_clock_zone, which main deleted. This branch's
  file_mtime_survives_a_reboot and file_mtime_undated are kept beside
  them.
- rust: main's pin 9c3eea441d8 (#597) is merged into the fork's
  wt-toyos-mtime as 90697f1401a, with no conflicts.
- issues/filesystem/a-fat-files-mtime-reads-finer-...: "two seconds of
  local time" is no longer true, so it now reads "two-second units".

issues/kernel/sys-clock-realtime-is-now-a-format-of-sys-clock-epoch.md
is unchanged. Both handlers still read clock::utc_secs, and its value
did not change. This branch adds no caller of clock_realtime.

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

Japabu commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 3, at 389f572

CI host concluded success at 389f572. The orchestrator's guest runs at 389f572 exited 0: 202_stat_mtime, file_mtime, file_mtime_undated, file_mtime_survives_a_reboot, wall_clock_rtc_dead, wall_clock_rtc_unstable, kernel_log_file and wall_clock_utc. These exited 1: nlink-u32 on 202_stat_mtime, site-114 on file_mtime, undated on file_mtime_undated, and the whole-change control on file_mtime. The Fast tier run was invalid, so it counts neither way. gh pr view 587 reports MERGEABLE/CLEAN against 7e15181.

Net size is +582/−35. Production is +67/−31: kernel +42/−21, toyos-abi +5/−1, libc +19/−8, and the rust pin 1/1. Tests are +380/−2 and issues +135/−3.

Round-1 BLOCKERs

  • st_nlink: u32 / the C layout: CLOSED. 202_stat_mtime exits 0, and the nlink-u32 mutation exits 1.
  • The three unobserved stamp sites: OPEN, 2 of 3. :114 is closed by the site-114 mutation (exit 1). No measurement exists for :132 or :821. The tests that should catch them exist (the MISSING and a truncation arms of file_mtime), but neither mutation was run. Run each alone and require file_mtime to exit 1:
    • kernel/src/object/ops.rs:132: let mtime = crate::clock::mtime_now(); → let mtime = crate::clock::nanos_since_boot();
    • kernel/src/object/ops.rs:821: state.mtime = crate::clock::mtime_now(); → state.mtime = crate::clock::nanos_since_boot();
  • The no-RTC clause untested: CLOSED. The contract is now 0 = undated, file_mtime_undated exits 0, and the undated mutation exits 1.
  • Round-1 REMOVEs: all closed.

BLOCKER

  • kernel/src/fat32_adapter.rs:1055: update_metadata now stores the flushing handle's mtime where main stored the flush instant. That is a regression on FAT. A writer that does not fsync closes first, then a reader closes last. The reader's close runs the flush (object/file.rs:23 → writeback.rs:112) with the reader's open-time stamp, so the file is stored with its old mtime and a build tool does not rebuild. Round 1 made this a BLOCKER if the landing order changed, and it has: Storage: file servers for DATA, the log and the boot volume; the kernel's NVMe and FAT go #536 is still OPEN and this lands first, so the FAT adapter stays on main. Fix: update_metadata stamps now() as on main (create keeps stamp(mtime)), and delete the-last-handle-…md:21-23.
  • kernel/src/fat32_adapter.rs:926: no test can fail on the FAT row of the new Stat::mtime contract. .map(|m| m.modified_unix.saturating_mul(crate::clock::NANOS_PER_SEC)) → .map(|m| m.modified_unix) stays green, because wall_clock_utc reads the entry on the host and nothing stats a FAT file through the kernel. With that mutation, a reopened FAT file reads 1970 plus about 2 s. A reader-last flush then stores stamp(secs), which clamps to 1980. The adapter now outlives this landing, so this is a filesystem claim with no check. Add a /log arm to file_mtime (tests already write /log, e.g. writeback_durability): write, close, reopen, then require (before−1)·10⁹ ≤ stamp ≤ after·10⁹ and stamp % 10⁹ == 0. It must go red under that patch.

NOTE

  • issues/isolation/the-so-caches-identity-is-blind-on-an-undated-machine.md is the same defect, with the same exit condition, as the existing section "The identity cannot see a same-size rewrite" in the-so-caches-refusals-are-narrower-than-its-reach.md. Keep one file.
  • rust library/std/src/sys/fs/toyos.rs:553,567: stat of a directory and lstat of a symlink answer mtime 0. Since ac63a08077f, modified() on them is Err(Unsupported) where it was Ok. std-calls-undated-what-it-did-not-stat.md names only DirEntry::metadata and set_times, so this new refusal is on no record.
  • kernel/src/clock.rs:163: pub const NANOS_PER_SEC has one caller outside clock.rs (fat32_adapter.rs:660,926), and Storage: file servers for DATA, the log and the boot volume; the kernel's NVMe and FAT go #536 deletes that caller. Make it private at that merge.

REMOVE

  • issues/isolation/the-so-caches-refusals-are-narrower-than-its-reach.md:22: "kernel/src/fat32_adapter.rs:849 stamps whatever now() gives" is false from this branch on, because FAT stamps the VFS's mtime.
  • PR body, "When merging Storage: file servers for DATA, the log and the boot volume; the kernel's NVMe and FAT go #536 (fsd)": process notes for another PR; they rot at the merge and do not belong in main's record.
  • PR body, the "Guest runs, by the orchestrator at 47e09802" table and "389f572c has not been booted; wall_clock_utc is the new test to run against it": stale.
  • PR body, "Each is a checked patch in mtime-r4/" and "Logs are in the orchestrator's scratchpad under mtime-r4/": the scratchpad is gone.
  • PR body, the paragraph "issues/kernel/sys-clock-realtime-is-now-a-format-of-sys-clock-epoch.md is unchanged by this branch…": it narrates something the branch did not do.

SEND BACK

🤖 Generated with Claude Code

https://claude.ai/code/session_01U6SVYFkdvV2t38KzNrESxs

Japabu and others added 2 commits September 29, 2026 13:00
Review round 3 on #587:

- fat32_adapter.rs: update_metadata stamps now(), the flush's own instant,
  as main did, not the flushing handle's mtime. The last handle to close
  can be a reader opened before the last write, so its mtime would store
  the file as older than its contents. `create` still stamps the mtime the
  VFS hands it. Stat::mtime's doc says FAT's two seconds are the flush's,
  and the last-handle issue's paragraph saying the defect reached FAT goes.
- file_mtime's shared run writes /log/file-mtime, reopens it and requires
  its stamp within (before-1)*1e9 ..= after*1e9 and on a whole second, so
  dropping the adapter's seconds-to-nanoseconds conversion reds it.
- the-so-caches-identity-is-blind-on-an-undated-machine.md merges into the
  same-size-rewrite section of the-so-caches-refusals-are-narrower-than-its-
  reach.md, whose false fat32_adapter.rs:849 clause goes.
- std-calls-undated-what-it-did-not-stat.md also records that a
  directory's stat and a symlink's lstat answer mtime 0, so modified()
  on them is Err(Unsupported).
- a-fat-files-mtime-reads-finer...md loses "at or below it" and "the one
  a later reader sees is the older", false now FAT stamps at the flush.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U6SVYFkdvV2t38KzNrESxs
@Japabu

Japabu commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator Author

Mutation patches for round 5, head f7caded3. Each applies alone with git apply (checked), builds with cargo run -- --build-only (exit 0 at 28f7ae94 and again at the merge f7caded3), and was reverted with the tree left clean. Not booted: the guest runs are the orchestrator's.

m132-ops.patch — `kernel/src/object/ops.rs:132`: a create of a missing file stamps `nanos_since_boot()`. Test: file_mtime; expected exit 1

Expected: file_mtime (Fast, shared boot) exits 1: the MISSING arm's stamp is far below before·10⁹.

diff --git a/kernel/src/object/ops.rs b/kernel/src/object/ops.rs
index 40f6143e..59d0817d 100644
--- a/kernel/src/object/ops.rs
+++ b/kernel/src/object/ops.rs
@@ -129,7 +129,7 @@ pub fn open(table: &mut HandleTable, path: &str, flags: OpenFlags) -> u64 {
                     (file_id, mtime, position)
                 }),
                 Err(SyscallError::NotFound) if create => {
-                    let mtime = crate::clock::mtime_now();
+                    let mtime = crate::clock::nanos_since_boot();
                     vfs.create_file(target.as_str(), mtime).map(|file_id| (file_id, mtime, 0))
                 }
                 Err(e) => Err(e),
m821-ops.patch — `kernel/src/object/ops.rs:821`: `ftruncate` stamps `nanos_since_boot()`. Test: file_mtime; expected exit 1

Expected: file_mtime (Fast, shared boot) exits 1: the a truncation arm's stamp is far below before·10⁹.

diff --git a/kernel/src/object/ops.rs b/kernel/src/object/ops.rs
index 40f6143e..785096c6 100644
--- a/kernel/src/object/ops.rs
+++ b/kernel/src/object/ops.rs
@@ -818,7 +818,7 @@ pub fn ftruncate(object: &KObjectRef, size: u64) -> u64 {
     }
     // The seek pointer is not touched (POSIX ftruncate): a shrink leaves it past EOF.
     file.with(|state| {
-        state.mtime = crate::clock::mtime_now();
+        state.mtime = crate::clock::nanos_since_boot();
         0
     })
 }
m926-fat-seconds.patch — `kernel/src/fat32_adapter.rs:926`: `file_mtime` answers FAT's seconds unconverted. Test: file_mtime; expected exit 1

Expected: file_mtime (Fast, shared boot) exits 1: the /log arm reads its whole seconds back as nanoseconds, far below (before−1)·10⁹.

diff --git a/kernel/src/fat32_adapter.rs b/kernel/src/fat32_adapter.rs
index 57a4a153..32cf34e5 100644
--- a/kernel/src/fat32_adapter.rs
+++ b/kernel/src/fat32_adapter.rs
@@ -923,7 +923,7 @@ impl FileSystem for FatFs {
         let role = self.role;
         self.fs
             .metadata(name)
-            .map(|m| m.modified_unix.saturating_mul(crate::clock::NANOS_PER_SEC))
+            .map(|m| m.modified_unix)
             .map_err(|e| refused(role, &self.fs, &mut self.repair_named, "metadata", name, e))
     }
 

@Japabu

Japabu commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 4, at f7caded

CI host concluded success on the check run at f7caded3. The orchestrator's guest runs at f7caded3 exited 0: file_mtime (with its /log arm), 202_stat_mtime, and under --weekly file_mtime_undated, file_mtime_survives_a_reboot, wall_clock_utc, wall_clock_rtc_dead, wall_clock_rtc_unstable and kernel_log_file. The Fast tier also exited 0. Run alone, each of m132-ops, m821-ops and m926-fat-seconds made file_mtime exit 1. f7caded3 merges 4de5ecdb with no conflict hunk (git show --cc is empty). gh pr view reports MERGEABLE/CLEAN, and main has since moved to d1d83f64, which touches only src/redlist.rs and two issue files.

Net size is +586/−38. Production is +67/−28: kernel +42/−18, toyos-abi +5/−1, libc +19/−8, and the rust pin 1/1. Tests are +397/−2, counting the toyos-fat32 unit test, and issues are +122/−8.

Round-3 BLOCKERs

  • ops.rs:132 / ops.rs:821 unobserved: CLOSED. m132-ops and m821-ops each made file_mtime exit 1.
  • fat32_adapter.rs update_metadata stored the flushing handle's mtime: CLOSED. :1057 is let time = now(); again, which is main's line with only a comment added. The lines the-last-handle-…md gave to FAT are deleted.
  • fat32_adapter.rs:926 had no test on the FAT row: CLOSED. m926-fat-seconds made file_mtime exit 1 on the /log arm.

Round-3 NOTEs: the so-cache issue files are merged, and the dir/symlink refusal is recorded in std-calls-undated-…md. The NANOS_PER_SEC visibility NOTE is deferred to the #536 merge, as it was written. Round-3 REMOVEs: all five are gone.

Negative control

Running it at 389f572c onto 7e151819 is enough; nothing needs to be run again. It paired with the green arm measured at 389f572c. Nothing it depends on has moved since:

  • git diff 7e151819 4de5ecdb changes no line that matches mtime|nanos_since_boot, so main's stamp sites are byte-identical at the new base.
  • The red assertion (file_mtime's first judged) is unchanged since 389f572c. Round 5 only appends the /log arm.
  • Round 5 moves production toward main (update_metadata), so the change is a subset of what was controlled.
  • The one new arm has its own measured mutation (m926).

At 32948daf the whole-change control also reddened file_mtime_survives_a_reboot, and that test's host-side bcachefs reader against the staged -rtc base= is the independent oracle.

BLOCKER

None.

NOTE

  • kernel/src/fat32_adapter.rs:1057: the mutation - let time = now(); / + let time = stamp(_mtime); stays green on every test. The /log arm's only handle is its writer, so the reason the new comment gives has no check. Because this is main's behaviour restored, it is a NOTE. The test that would catch it: open /log/x for reading, wait on epoch() advancing 2 s (not a sleep), write and close through a second handle, close the reader last, reopen, and require a stamp of at least (before_write−1)·10⁹.
  • issues/filesystem/a-fat-files-mtime-reads-finer-through-its-writer-than-after-a-reopen.md:15-16: the exit condition says "rounded to the mount's precision at the write". FAT now stores the flush's instant (fat32_adapter.rs:1057), so a handle rounded at the write still disagrees with what the mount stores, and meeting the condition as written would not close the defect.

REMOVE

  • kernel/src/fat32_adapter.rs:663: "The wall clock now, as [stamp] stores it." The false line was rewritten when it should have been deleted, and it narrates a one-line body.
  • PR body, "file_mtime_undated goes red because modified() answers 1970 plus uptime" under Negative control: the whole-change control was never run on that test. It was run on file_mtime at 389f572c and on file_mtime/file_mtime_survives_a_reboot at 32948daf. The undated mutation is the measured claim, and it is already listed.

LAND AFTER NAMED CHANGES

🤖 Generated with Claude Code

https://claude.ai/code/session_01U6SVYFkdvV2t38KzNrESxs

Japabu and others added 2 commits September 29, 2026 13:52
…dition checkably

Delete the doc line on `now()`. Rewrite the exit condition of the FAT mtime
issue so it names the check, and record there that the flush's own instant
(`now()` vs the handle's `_mtime`) is untested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U6SVYFkdvV2t38KzNrESxs
@Japabu
Japabu added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit 52ee39b Sep 29, 2026
1 check passed
@Japabu
Japabu deleted the wt/toyos-mtime branch September 29, 2026 12:17
Japabu added a commit that referenced this pull request Sep 29, 2026
Brings #587 (a file's mtime is UTC nanoseconds since the Unix epoch),
#603, #614 and #615.

Conflicts, every hunk of both sides:

- kernel/src/fat32_adapter.rs (modify/delete): the deletion stands.
  #587's hunks there are `stamp(mtime)` as whole UTC seconds, `now()` as
  `stamp(mtime_now())`, `create` stamping the VFS's mtime, the flush
  stamping its own instant, and `file_mtime` answering seconds times 10^9.
  FAT stamping is fsd's on this branch: its `lstat`, `list` and
  `node_meta` already answer seconds times 10^9, its level stamps the
  flush's own instant, and the next commit gives it the nanosecond clock
  to divide.
- kernel/src/vfs.rs: this branch deleted `open_backing_identified`'s
  flush of a dirty file, so #587's `mtime_now` there goes with it. #587's
  `file_mtime` doc hunk is taken.
- kernel/src/object/ops.rs: the write and `ftruncate` stamps keep this
  branch's `file_cache::touch` and `set_size`, and stamp `mtime_now`; the
  two open stamps merged clean. #587's comment on dirty state in the cache
  is not taken: this branch's cache keeps no dirt.
- kernel/src/leak_selftest.rs: this branch deleted `fat_reopen_census`
  with the FAT adapter it probed, so #587's `mtime_now` there goes too.
- kernel/src/clock.rs (merged clean): `NANOS_PER_SEC` is private, since
  the FAT adapter it was made public for is gone.
- issues/isolation/the-so-caches-refusals-are-narrower-than-its-reach.md:
  this branch deleted the FAT same-size-rewrite section, because the
  kernel's cache reaches no FAT mount. #587's undated paragraph is still
  true of `/tmp`, the one writable mount a kernel `dlopen` reaches, so the
  section stays for it, with #587's exit condition.
- rust: c4c65e3e87a on ToyOSOrg/rust wt-toyos-fsd merges main's pin
  90697f1401a with no conflict.

Issues arriving with main, against this tree:

- two-shared-members-assume-a-bcachefs-home-the-t14-does-not-have.md
  (#615) is deleted. Its premise is the kernel's tmpfs `/home` and
  `NvmeBacking`, and both are gone: on the T14 `/home` is fsd's bcachefs in
  memory. The T14 run at 27b5641 reads `fs_large_file` exit=0 and
  `home_backing_revoked` exit=0 beside fsd's "this machine has no DATA
  partition; ... are in memory" line.
- the-last-handle-to-close-stamps-the-file-with-its-own-mtime.md is
  deleted. Its mechanism is the kernel write-back's last-close flush, which
  this branch deletes. `/tmp`'s mtime is the file's (`file_cache::touch`),
  and fsd's DATA keeps one mtime per node. What stays true of a handle's
  mtime is filed as a-kernel-files-fstat-answers-its-handles-mtime.md.
- a-fat-files-mtime-reads-finer-through-its-writer-than-after-a-reopen.md
  is deleted. Its exit condition's second arm holds here: the kernel mounts
  no FAT volume a process writes. fsd's FAT reads the entry's stamp through
  a handle, which differs from a reopen's the other way. That is filed as
  a-fat-files-mtime-through-its-handle-is-its-last-level.md.
- std-calls-undated-what-it-did-not-stat.md: the sentence that std reads
  an mtime only off SYS_FSTAT is deleted, since a served file's comes off
  fsd's stat.
- tests/common/wallclock.rs: `mtime_boot`'s tmpfs guard looked for the
  kernel's "are a tmpfs", which nothing prints here. It reads fsd's
  `storage::IN_MEMORY` line instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U6SVYFkdvV2t38KzNrESxs
Japabu added a commit that referenced this pull request Sep 29, 2026
#587's contract: a file's mtime is nanoseconds since the Unix epoch, UTC,
DATA keeps the nanosecond, and 0 is undated. fsd stamped DATA with
`clock_epoch()` times 10^9, whole seconds, so two writes to `/home`
inside one second carried one mtime. It also read the clock a second
time for FAT (`utc_secs`), with the same undated rule in another unit.

- `fsd::volume::now_nanos` is the one reader. It derives the kernel's
  anchor exactly: `SYS_CLOCK_EPOCH` is `BOOT_SECS` plus the whole seconds
  of the counter at the call, and the process's clock page is the same
  counter on the kernel's formula, so a call bracketed by two readings
  inside one second names `BOOT_SECS`. A bracket that straddles a second
  is asked again, and three that straddle panic by name. The anchor is
  taken once: the kernel sets it once, before the first process. A
  refused clock is undated, 0.
- FAT's volume takes the same clock and divides it, as main's
  `stamp(mtime_now())` did. `utc_secs` is deleted.
- `file_mtime` judges `/home` too: two writes, the second strictly
  later, and not both whole seconds. `file_mtime undated` writes `/home`
  beside `/tmp` and requires both undated.
- The host test `the_anchor_is_the_kernels` holds the derivation to the
  kernel's arithmetic, a straddled bracket included.
- userlands-wall-clock-...md: the sentence that a file server can stamp
  only whole seconds is deleted; fsd stamps nanoseconds now.
- home_overwrite_zero.rs: the comment that `fs::metadata` is the kernel's
  `file_cache::size` is deleted; `/home` is fsd's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U6SVYFkdvV2t38KzNrESxs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant