Repository navigation
Conversation
eiffel-init now does what chronyd did, in-process. /dev/ptp0 is the host's CLOCK_REALTIME as ptp_kvm exposes it: reading it is the KVM clock-pairing hypercall, answered by the hypervisor rather than by the parent instance, so the parent cannot skew an enclave's clock. One sample is taken between mounting the filesystems and spawning anything, and the clock is stepped if it is more than a millisecond out, so no process ever observes time jumping backwards. After that a thread of its own samples every 4s — chrony's poll 2 on the same refclock — and feeds the offset to the kernel PLL, which slews. The supervisor stays single-threaded. PTP_SYS_OFFSET_PRECISE2 is a cross-timestamp and needs no correction for the measurement's own delay, but it answers EOPNOTSUPP wherever the pvclock TSC-stable bit is clear (nested virtualisation, mostly), so the bracketed PTP_SYS_OFFSET2 fallback is load-bearing rather than belt-and-braces. The ioctl request numbers and struct layouts are derived with nix's ioc macros from include/uapi/linux/ptp_clock.h in the kernel the Dockerfile pins, never from memory. A unit test checks them against .github/ptp-ioctl-numbers.txt, which CI regenerates from that header with a C snippet, so a uapi change fails the build rather than leaving an enclave unable to read the host clock. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
eiffel-init keeps the clock itself now, so the Dockerfile's chrony stage, /sbin/chronyd, /etc/chrony.conf, /etc/passwd (chronyd was the only thing that read it), the /var/lib/chrony mkdirs and /rootfs/var all go. The MUSL, GCC and BINUTILS pins stay: the kernel stage still compiles with them. The bootstrap ramdisk is now exactly /init, the CA bundle at /rootfs/etc/ssl/certs/ca-certificates.crt, and the directory entries. PCR0, PCR1 and PCR2 all move with this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… clock Two things CI did not cover before. The ioctl check compiles .github/ptp-ioctl-numbers.c inside the Dockerfile's kernel stage, against that tree's own include/uapi/linux/ptp_clock.h, and diffs the request numbers and struct sizes it prints against the file eiffel-init's unit test reads. A uapi change now fails the build instead of leaving an enclave unable to read the host clock. The boot test gains -rtc base=2000-01-01, which starts the guest's wall clock 26 years behind the host's while ptp_kvm still reports the host's real time. With KVM it asserts that init logged a step and that the date /app/hello printed is the host's; the console says whether the precise ioctl or the PTP_SYS_OFFSET2 fallback produced it, so both paths pass and a failure is diagnosable. Without /dev/kvm there is no hypercall and so no /dev/ptp0: the time assertions are skipped and the run instead proves a missing PTP clock does not stop the enclave booting. With chronyd gone nothing races /app/hello to the exit any more, so its clean-exit assertion no longer needs KVM either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
patrislav
force-pushed
the
feat/rust-init-time-sync
branch
from
November 2, 2026 16:15
ff0f2c9 to
cdd38d8
Compare
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings up the workflow hardening. No conflicts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The PTP ioctl check this branch adds unpacks a kernel source tree, which does not fit in the 20 minutes that covered lint, test and the musl build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
The enclave still carries a full NTP daemon for one job it is not doing: there is no network in here, so chronyd is not speaking NTP to anyone. It exists only to read the host's clock from
/dev/ptp0and discipline the local one against it. That is a build stage, a config file, a password file and a separate binary in the measured ramdisk, in exchange for a few hundred lines of work the init can do itself. This folds it in and deletes the rest.Last of three stacked pull requests; merge the other two first. After this the bootstrap ramdisk holds exactly the init binary and a CA bundle, plus directory entries, and everything measured into PCR1 is built from source in this repository.
Why this is enough
The only time source in the enclave is the host's clock, exposed through a hypercall. The guest clocksource derives from the same host TSC, so frequency drift is near zero and the real problem is the offset at boot. A step before any app starts, plus a slow correction loop, covers that. What chrony offered beyond this, statistical filtering and long-term frequency learning, does not matter for a clock whose reference sits on the same physical host.
Stepping before spawn also means no process ever observes time going backwards. chrony stepped on its first measurement, which could land after an app had already started.
What lands
time_syncmodule with no new dependencies. The ioctls are defined locally against the kernel's own header.A note on the fallback ioctl
The two PTP calls have different directions in the header, which is easy to get wrong. The precise one is read-write. The fallback is declared write-only even though the kernel writes timestamps back, so deriving its request number as read-write produces a number the kernel rejects. It is built as a write-pointer ioctl instead, with the reason in a comment.
Only the unsupported-operation error triggers the fallback, deliberately. A bad-ioctl error would mean the request number is wrong, and falling back there would hide exactly the mismatch the tests exist to catch.
Verification
Not verified
No ioctl was ever issued against a real PTP device, so neither sampling path has run end to end; only the numbers and the pure math are tested. Nothing was booted, on Nitro or under QEMU. The CI assertion that the guest clock gets stepped rests on an assumption about how QEMU's clock base reaches the guest that has not been demonstrated; if it does not hold, that step needs rethinking.
The frequency estimator ships enabled without field data behind that decision. It is the one piece approximating what chrony did, it is bounded, and the comment in the source names the ceiling: the kernel is slewing the clock while the estimator measures it, so part of what it reads as drift is its own correction. Turning it off is a one-line change.
Rollback
Reverting this restores chrony with no other change. The two earlier pull requests do not depend on it.
Impact
PCR0, PCR1 and PCR2 move once more. Re-pin from the PCR output.
🤖 Generated with Claude Code