Repository navigation
feat(computers): agent-env's libfaketime for the computer's clock, gated on x86_64 and arm64 (FD-5474) - #619
Merged
earakely-scale merged 8 commits intoNov 12, 2026
Conversation
…ted on x86_64 and arm64 (FD-5474) src/agent_env/computers/clock is the library every computer's session programs will load to read universe time: upstream libfaketime 0.9.12 from its sha256-pinned tarball with agent-env's changes, as a small libfaketime.patch and two engine files it includes (agentenv_clock_page.c, agentenv_clock_waits.c; GPL-2.0-only, as libfaketime is), built by build.sh, with the page writer the follower uses (clockpage.py) and the library's default settings (clock.conf). Clock reads and waits for a set realtime clock time follow the universe; monotonic and boot clocks and every relative wait stay real; only the session user is faked; settings are read once at load. A clock read now copies the page before reading the real time and keeps the pair only if the page is still the one copied: a reader that waited out a write paired a time from before it with the page it wrote, which stepped CLOCK_REALTIME back by up to ~280 ns at a rate change. tst/clock (make clock-test, .github/workflows/clock.yml on x86_64 and arm64) builds it and stock 0.9.12 in an Ubuntu 24.04 image and gates it: every wait API in C, Python, Java and Node at several rates, a rate change, a set_time back and 30 jumps, condvar and semaphore stress, a stalled writer, torn reads, who is faked, the marker, the environment race and Chromium launched directly 60 times (stock 0.9.12 as the positive control), in under 30 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s by file, a dead page writer (FD-5474) Greptile's review of the clock library: - a condvar wait cancelled mid-wait gives up its table entry (cleanup handler); - sem_timedwait and the universe clock_nanosleep are cancellation points again, and a cancelled semaphore wait leaves the waiter count as it found it; - without futex_waitv (before Linux 5.16, gVisor, a seccomp filter), realtime futex and semaphore waits wait on the word in slices and plan again from the page between them, instead of a deadline translated once or not at all; - a timerfd's record holds its descriptors, kept through dup, dup2, dup3, fcntl, close and close_range; - clockpage.write takes up a page a dead writer left with seq odd; - clock.conf and the README say settings are file-only while the file exists. The gates check every wait's return code, and new gates cover cancellation, timerfd duplicates, a dead writer, and the waits again in containers that refuse futex_waitv. The patch's headers are a/ and b/ again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eep a timerfd's record, Chromium variants side by side (FD-5474) - Without futex_waitv, a slice's end is the only reason to read the page again; any other futex error (EINVAL for an unaligned word) is the caller's, and futex_waitv is taken as missing only on ENOSYS or a seccomp EPERM. - dup2 and dup3 that replace a timerfd, and close_range that closes one, make the call and its bookkeeping under the table lock, so a failed call keeps the record. - The Chromium gate runs its 60 launches with this library and stock's 10 in containers side by side: CI's amd64 runner took 32.6 s in sequence, over the 30 s budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s write (FD-5474) The page changes, and waits wake, before the write's last step (the clock file); on CI's amd64 runner that step once took 55 ms, so waits a jump had ended correctly read as 55 ms early against the write's end. A wait a jump or a change ends must now end no earlier than its write started and within its bound of the write's end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s 30 s on CI's amd64 runner (FD-5474) 60 launches alone took 31.4 s there on one run. 40 clean launches are still conclusive: at stock's one crash in five, a run would come out clean about once in 7,500. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ords alone (FD-5474) A child that shares its parent's memory until exec (vfork, as Python's subprocess uses) has its own descriptor table, but its close and close_range went through the library's bookkeeping and erased the parent's timer records: one subprocess.run(["true"]) left a Python program's clock-time timerfd ignoring a 2 h jump. The tables now belong to the process that loaded the library, or to the child of each glibc fork, and only it changes them. The timerfd gate covers a vfork child's close and close_range, and a Python program's subprocess.run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing gates bounded by failure, and a real desktop test (FD-5474) - A fork while another thread held the tables' lock (closing or duplicating a timerfd) left the child stuck on it: 12 of 300 children on the x86 host. The load-time fork hook now frees it in every child, and a gate forks 1,000 times while a thread works a timerfd. - The gates' lateness bounds are now tst/clock/bounds.py's: a bound a broken library misses by seconds (a jump it never saw, a rate it ignored, a lost wakeup) and a slow runner doesn't reach. Early is never allowed, and return codes, losses and torn reads stay exact. The downloads and the image build are tried three times. - tst/integration/computer/clock_desktop_test.py: the library composed into a real computer (ubuntu-24.04, the reference driver, agentenv's chromium and a test app that builds and preloads it) passes the conformance probe at full, and with its page written as root, a shell, Chrome's JavaScript and the driver's input follow 30 jumps, x60 and a freeze, with Chrome, Xvfb and the panel running on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te starts (FD-5474) It was timed from after the write and the shell's read, so it could undercount by however long those took. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
earakely-scale
deleted the
edgararakelyan/fd-5474-hb14-patched-libfaketime-for
branch
November 12, 2026 02:23
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of FD-5420, the computer's own clock following the gateway's universe clock. This is FD-5474: the library every computer's session programs will load to read universe time. Every program on a computer reads its own clock, so it can't ask the gateway's
get_timethe way an MCP server does. Instead, agent-env's build of libfaketime answers those programs' clock reads and clock-time waits from a page a follower process keeps in step with the gateway. Wiring the library into the base computer (/etc/ld.so.preload, the follower, supervisord) is FD-5476.What it is
src/agent_env/computers/clock/:libfaketime.patchsrc/, applied withpatch -p1agentenv_clock_page.clibfaketime.cagentenv_clock_waits.cCOPYINGbuild.shlibfaketimeMT.so.1from upstream's sha256-pinned tarball and the files aboveclockpage.pyclock.confWhat the library does:
CLOCK_REALTIME,_COARSE,_ALARM, TAI,gettimeofday,time) and waits for a set realtime clock time (clock_nanosleep TIMER_ABSTIME, realtime futex waits,sem_timedwaitandsem_clockwait, realtime condvars, absolute realtime timerfds and POSIX timers). Each wait is planned in real time and planned again whenever the page changes.futex_waitv(before 5.16, or gVisor), realtime futex and semaphore waits read the page between 100 ms slices instead.dup,dup2,dup3,fcntl,closeandclose_rangeare interposed), not by its number.AGENTENV_REAL_TIME=1stays real, and its children are faked./etc/agentenv/clock.conf, or from the environment where there's no such file. While the file exists, the environment changes none of them, so a session program can't point the library at a page of its own. Stock libfaketime'sgetenvraced programs' ownsetenv; that crashed Chromium on about one launch in five and hangs it outright in headless mode.The patch and the engine files are the prototype's library, moved out of the two Python scripts that applied it as string replacements (about 900 lines of C inside a Python string). Their preprocessed source was byte-identical to the prototype's before the fix below.
A bug the gates found, fixed here
The gates found it on arm64: with the follower stalled 5 ms in a write,
CLOCK_REALTIMEstepped back up to 280 ns at a rate change, 3 to 7 times per 2,000 writes.ft_snap_at). The real time is ordered before that check: a dependent load anddmb ishldon aarch64,lfenceon x86_64.Greptile's first review, fixed here
All seven findings were real; each has a gate now.
sem_timedwaitused raw futex calls. It now checks for cancellation first, cancels asynchronously for the system call alone, and a cleanup handler takes the thread back off the waiter count.clock_nanosleep's universe sleep does the same.futex_waitv: the fallback translated a deadline once, or not at all, and stopped watching the page. It now waits on the caller's word in slices of at most 100 ms and plans again from the page between them. This path is live on Modal: a probe found its default sandboxes (gVisor, kernel4.19.0-gvisor) answerfutex_waitvwith ENOSYS, while itsruntime="vm"sandboxes (kernel 7.2) have it.dupandclose, the helper re-armed a closed number or another timer. Records now hold the timer's descriptors.seqodd, and every later write stayed odd.clockpage.writenow goes on from the next even value and skips the continuity rule for that write.clock.confsaid the environment could override the file. It can't, on purpose, and the docs now say so.Greptile's second review (4/5) found two more, also fixed:
dup2,dup3orclose_rangethat failed still dropped the target timer's record. Calls that replace or close a timerfd now run with their bookkeeping under the table lock, so a failed one keeps the record.futex_waitv, a futex error such as EINVAL for an unaligned word was retried as a page wake, spinning a core. Such errors now go back to the caller.futex_waitvis taken as missing only on ENOSYS, or EPERM from a seccomp filter.A review of my own after that found one more, from the timerfd fix:
vforkchild broke its parent's timers: such a child shares its parent's memory until it runs its program, so itscloseorclose_rangeerased the parent's timer records while the parent's timers stayed open. Python'ssubprocessstarts children this way: onesubprocess.run(["true"])left a Python program's clock-time timerfd ignoring a 2 h jump. The tables now belong to the process that loaded the library, or to the child of each glibc fork. A child that shares memory without forking has a descriptor table of its own, and its calls change nothing in them.A second review of my own found one more, also from the timerfd fixes:
close/dupbookkeeping takes the library's table lock, but a fork's child had it reset only once the helper thread had started. A fork while another thread held it left the child waiting forever: 12 of 300 children on the x86 host. The load-time fork hook now frees it in every child.The patch's headers are back to
a/andb/without timestamps; a regeneration on macOS had leftpa/andpb/in them.Where it lives
It's at
computers/clock/, beside the bundle's other shared recipes (disk/,vm_bundle/), not atartifacts/ubuntu-24.04/clock/as the issue said. Inside the base computer's folder it would change the base's build context, and with it the image's tag, though nothing loads the library yet. FD-5476 brings it into the base.Licensing
The patch and the two engine files are derived from libfaketime, so they carry
GPL-2.0-onlyand upstream'sCOPYINGsits beside them. Everything else in the folder and the repo stays Apache-2.0, and the package's license metadata is unchanged.THIRD_PARTY_NOTICES.mdgets a libfaketime section. I also changed its opening sentence, which said no third-party source is incorporated in the repository; that's no longer true for these three files. That sentence is legal wording, so please check the change. Whether the wheel's license metadata should also list GPL-2.0 for these files is a question for legal.Tests
tst/clock(make clock-test,.github/workflows/clock.ymlonubuntu-24.04andubuntu-24.04-arm, not a required check):browsersbundle's pin, read fromchromium.py) and upstream's tarball are fetched once into a cache, checked by sha256, and kept between CI runs.The gates:
futex_waitv: the waits, rate-change, jump, cancellation and semaphore-stress gates run a second time in containers whose seccomp profile answersfutex_waitvwith ENOSYS, as gVisor and kernels before 5.16 do. A probe checks that it does.sem_timedwaitandclock_nanosleepgo at once, and the semaphore keeps no waiter. 600 threads are cancelled inpthread_cond_timedwait, more than the table holds. A condvar wait after them still ends at a jump past its deadline. A realtime futex wait on an unaligned word fails with EINVAL at once.dup,dup2,dup3,fcntl(F_DUPFD_CLOEXEC)andclose_range. A timerfd armed an hour ahead is duplicated and closed, and its number goes to a duplicate of a monotonic timerfd. A 2 h jump fires the duplicate at the jump and leaves the monotonic one alone. A case makesdup2,dup3andclose_rangecalls on the timerfd fail, and three have avforkchild close it: withclose, withclose_range, and through a Python program'ssubprocess.run. In each, it still fires at the jump.sem_timedwaitposts, with the page changing under them, lose nothing; every handoff is under 2 ms.seqodd is taken up by the next write, and session programs read it.CLOCK_REALTIME: 0 torn reads in about 319 million on arm64.setenvracing clock reads and timed waits, 3 runs of 10 s each: no crash.How the gates judge time: correctness is exact (no wait ends early, every wait returns what it should, nothing is lost or torn). Lateness is held to
tst/clock/bounds.py, 1 s, which a broken library misses by seconds or hours and a slow runner doesn't reach. CI's amd64 runner had tripped the old millisecond bounds twice. The downloads and the image build are tried three times.On a real computer,
tst/integration/computer/clock_desktop_test.py(in the slow integration tier):dateis the universe;Date.now()holds;Results:
clock.ymland the clock desktop test included. One slow shard first failed inbrowser_computer_test's Playwright MCP test, which fails the same way onmain(its two latest failing runs); it passed on a rerun. That job passes all 28 tests on amd64 (10 min) and arm64 (9 min 45 s). Before 7690e0d, amd64 failed once on a jump round's measurement (fixed in e193801) and once on the Chromium time (fixed in 7690e0d).Docs
The docs site should cover the computer's clock: what follows universe time and what stays real, who is faked,
AGENTENV_REAL_TIME, andclock.conf. That page belongs with FD-5476, when computers load the library.🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge; no blocking issue remains from the supplied review threads or the latest change.Summary
Adds the computer clock library, clock-page writer, and clock gates for x86_64 and arm64. Loading the library in base computers remains a follow-up.
Reviews (11) · Last reviewed commit: "test(computers): time Chrome's showing a..." · Reviewed by Greptile