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
16 changes: 8 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31
with:
install_url: https://releases.nixos.org/nix/nix-2.35.1/install
- run: nix develop --command cargo fmt --all -- --check
- run: nix develop .#light --command cargo fmt --all -- --check

lint:
name: Lint
Expand All @@ -30,7 +30,7 @@ jobs:
with:
install_url: https://releases.nixos.org/nix/nix-2.35.1/install
- run: bash -n .agents/dev .agents/setup scripts/*.sh
- run: nix develop --command cargo clippy --locked --workspace --all-targets --all-features -- --deny warnings
- run: nix develop .#light --command cargo clippy --locked --workspace --all-targets --all-features -- --deny warnings

test:
name: Test (${{ matrix.os }})
Expand All @@ -45,7 +45,7 @@ jobs:
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31
with:
install_url: https://releases.nixos.org/nix/nix-2.35.1/install
- run: nix develop --command cargo test --locked --workspace --all-targets --all-features
- run: nix develop .#light --command cargo test --locked --workspace --all-targets --all-features

qemu-replay:
name: QEMU record/replay
Expand Down Expand Up @@ -105,12 +105,12 @@ jobs:
# and fails unless every requested run completes, the verdict is
# `reproducible`, and the guest measurements equal the reference host's.
- run: nix develop --command ./scripts/time-model-gate.sh
# The known record-mode result: with rr=record and sleep=off, QEMU queues
# live serial input and never flushes the queue. Asserting it here makes a
# QEMU change that starts delivering input visible, because that would
# unblock pinning the model in the product.
# The pinned model delivers input because the pinned emulator carries the
# replay-flush patch: without it, QEMU queues live serial input with
# sleep=off and never flushes the queue, so a recording cannot be driven.
# Asserting delivery here keeps that patch load-bearing.
- name: Pinned model host-input assertion
run: nix develop --command ./scripts/time-model-input-probe.sh --expect not-delivered
run: nix develop --command ./scripts/time-model-input-probe.sh --expect delivered
- run: nix develop --command ./scripts/phase4-acceptance.sh
- if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
Expand Down
48 changes: 25 additions & 23 deletions crates/simferret/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,26 @@ const MEMORY_MIB: u32 = 128;
/// The QEMU time model that record and replay both launch. `shift=auto` lets
/// QEMU adjust the virtual CPU speed against host real time.
///
/// RFD 4 pins `shift=4,sleep=off`, and Phase 0 validated that model as
/// reproducible on the reference host, but the product cannot launch it yet:
/// with `rr=record`, QEMU queues live serial input as a replay asynchronous
/// event and flushes the queue only from `icount_account_warp_timer()`, which
/// returns before `replay_async_events()` when `sleep=off`. A guest under that
/// model therefore never receives the control commands a recording is driven
/// with. `rfd/0004/EVIDENCE.adoc` records the measurement and the minimal
/// reproduction, and `rfd/0004/IMPLEMENTATION.org` records that pinning this
/// value is blocked on the RFD's model decision rather than on this code.
/// RFD 4 pins `shift=4,sleep=off`: a fixed scale, so the guest's virtual time
/// advances with the instructions it executes and its idle timers advance
/// deterministically rather than with host scheduling, and no idle sleep, so the
/// guest's clock is not paced by the host either.
///
/// Changing this value is a versioned change: recordings made under another
/// model are not comparable, and every host in the supported matrix must be
/// re-validated.
pub const TIME_MODEL: &str = "shift=auto";
/// The pinned QEMU carries `poc/time-model/qemu-nosleep-replay-flush.patch`,
/// applied by the flake's pinned emulator. With `sleep=off` alone, QEMU queues
/// live serial input as a replay asynchronous event and flushes the queue only
/// from `icount_account_warp_timer()`, which returns before
/// `replay_async_events()`; the patch moves the flush ahead of the sleep check,
/// so a recording can be driven. It is carried until upstream includes the fix,
/// and dropping it is a versioned change: recordings made under a different
/// emulator are not comparable, and every host in the supported matrix must be
/// re-validated. `rfd/0004/EVIDENCE.adoc` records the measurement, the pin, and
/// the spike that established the patch.
///
/// Changing this value is a versioned change for the same reason: the model is
/// part of VM identity, and recordings made under another model are not
/// comparable.
pub const TIME_MODEL: &str = "shift=4,sleep=off";
const START_TIMEOUT: Duration = Duration::from_secs(10);
const COMMAND_TIMEOUT: Duration = Duration::from_secs(10);
/// The largest number of command bytes written before the writer waits for the
Expand Down Expand Up @@ -1866,16 +1872,12 @@ mod tests {

#[test]
fn record_and_replay_launch_the_configured_time_model() {
// The literal is the model the product launches today. RFD 4 pins
// `shift=4,sleep=off` and Phase 0 validated that model on the reference
// host, but the product cannot launch it yet: with `rr=record` QEMU
// queues live serial input as a replay asynchronous event and only
// delivers it from `icount_account_warp_timer()`, which returns before
// `replay_async_events()` when `sleep=off`, so a guest under that model
// never receives the commands a recording is driven with. Pinning the
// model is therefore a versioned change that also updates this
// assertion and re-validates every supported host.
assert_eq!(TIME_MODEL, "shift=auto");
// The literal is the model the product launches. It is pinned by RFD 4
// and reaches QEMU through the pinned emulator, which carries the
// replay-flush patch `sleep=off` needs; changing either the value or the
// emulator is a versioned change that also updates this assertion and
// re-validates every supported host.
assert_eq!(TIME_MODEL, "shift=4,sleep=off");

let root = Path::new("/tmp/time-model");
for (mode, rr) in [
Expand Down
14 changes: 5 additions & 9 deletions docs/time-model.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ measurements. Current progress lives in the implementation checklist, not here.

| Implementation evidence
| link:../rfd/0004/EVIDENCE.adoc[RFD 4 evidence]
| The pinned-scale selection and its reason, the Phase 0 validation on the reference host, the record-mode limitation that blocks pinning the model, and the re-validation trigger
| The pinned-scale selection and its reason, the Phase 0 validation on the reference host, the record-mode limitation that the pinned emulator's carried patch resolves, and the re-validation trigger

| Validation gate
| link:../scripts/time-model-gate.sh[`scripts/time-model-gate.sh`]
Expand All @@ -42,15 +42,15 @@ measurements. Current progress lives in the implementation checklist, not here.

| Host-input probe
| link:../poc/time-model/input-probe.c[`poc/time-model/input-probe.c`] with link:../scripts/time-model-input-probe.sh[`scripts/time-model-input-probe.sh`]
| Whether a guest that waits for host input receives it under a given model, reported as `delivered`, `not-delivered`, or `inconclusive` from a guest that polls until the probe stops it, with the write ordered by acknowledged stop and resume commands on QEMU's monitor, and with a non-delivery claim requiring the emulator's output to have ended in one piece, the measurement that shows the pinned model does not deliver while record/replay is enabled, and the `--expect` mode the continuous-integration job uses to assert that known result
| Whether a guest that waits for host input receives it under a given model, reported as `delivered`, `not-delivered`, or `inconclusive` from a guest that polls until the probe stops it, with the write ordered by acknowledged stop and resume commands on QEMU's monitor, and with a non-delivery claim requiring the emulator's output to have ended in one piece, the measurement that shows the pinned emulator's carried patch delivers while record/replay is enabled, and the `--expect` mode the continuous-integration job uses to assert that result

| Host-input guest tests
| link:../poc/time-model/input-probe-test.c[`poc/time-model/input-probe-test.c`] with link:../scripts/time-model-input-probe-test.sh[`scripts/time-model-input-probe-test.sh`]
| The guest loop driven by stubs: a line that arrives after more than the polls the guest used to stop after is still reported as received, and the guest never reports giving up, so restoring either fails rather than passing

| QEMU patch spike
| link:../poc/time-model/qemu-nosleep-replay-flush.patch[`qemu-nosleep-replay-flush.patch`] with link:../poc/time-model/qemu-nosleep-replay-spike.nix[`qemu-nosleep-replay-spike.nix`]
| The minimal QEMU change that flushes queued replay events with `sleep=off`, and the scratch expression that builds the pinned QEMU with it; neither is wired into the repository's build, and the measurements are in the evidence document
| link:../poc/time-model/qemu-nosleep-replay-flush.patch[`qemu-nosleep-replay-flush.patch`] with link:../flake.nix[`flake.nix`]
| The minimal QEMU change that flushes queued replay events with `sleep=off`, applied by the flake to the pinned emulator; the measurements are in the evidence document, and the patch is carried until upstream includes it

| Probe source
| link:../poc/time-model/probe.c[`poc/time-model/probe.c`]
Expand Down Expand Up @@ -104,11 +104,7 @@ reports variation instead of failing on it:
predictions of production timing.
* Time, entropy, and ordering fault injection remain unimplemented, and the
arrival timing of live host input has no deterministic position in guest virtual
time; both are separate work.
* The pinned model cannot be adopted for record mode yet: with `sleep=off` and
`rr=record`, QEMU queues live serial input as a replay asynchronous event and
never flushes the queue, so a guest that waits for the control commands a
recording is driven with never receives them. The measurement, the models it
time; both are separate work.is carried by the pinned emulator and adopted by the product. The measurement, the models it
was compared against, and the source-level cause are in
link:../rfd/0004/EVIDENCE.adoc[RFD 4 evidence], and
`scripts/time-model-input-probe.sh` reproduces them. Continuous integration
Expand Down
37 changes: 36 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@
"x86_64-unknown-linux-musl"
];
};
# The pinned emulator, built from the repository's patch rather than from
# a second expression: `sleep=off` needs the replay flush the patch
# restores, and only the system target the project runs is built.
# `rfd/0004/README.adoc` records why it is carried and what dropping it
# means.
qemuPinned = (pkgs.qemu.override {
hostCpuTargets = [ "x86_64-softmmu" ];
enableDocs = false;
}).overrideAttrs (previous: {
patches = (previous.patches or [ ]) ++ [
./poc/time-model/qemu-nosleep-replay-flush.patch
];
});
in {
devShells.default = pkgs.mkShell {
nativeBuildInputs = [
Expand All @@ -40,7 +53,29 @@
pkgs.cpio
pkgs.gzip
pkgs.pkgsStatic.stdenv.cc
pkgs.qemu
qemuPinned
pkgs.xz
];
RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library";
shellHook = pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isLinux ''
export SIMFERRET_BUSYBOX="${pkgs.pkgsStatic.busybox}/bin/busybox"
export SIMFERRET_KERNEL="${pkgs.linuxPackages.kernel}/bzImage"
export SIMFERRET_KERNEL_MODULES="${pkgs.linuxPackages.kernel.modules}"
'';
};

# Jobs that do not run QEMU use this shell, so they do not build the
# pinned emulator. The QEMU job uses the default shell.
devShells.light = pkgs.mkShell {
nativeBuildInputs = [
rustToolchain
pkgs.jujutsu
pkgs.jq
pkgs.python3
] ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [
pkgs.cpio
pkgs.gzip
pkgs.pkgsStatic.stdenv.cc
pkgs.xz
];
RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library";
Expand Down
22 changes: 0 additions & 22 deletions poc/time-model/qemu-nosleep-replay-spike.nix

This file was deleted.

7 changes: 5 additions & 2 deletions poc/time-model/reference-host-values.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
# passing with a warning.
#
# The artifact identities are SHA-256 digests of the QEMU executable, the guest
# kernel, and the probe's initramfs. The QEMU digest rather than only its version
# kernel, and the probe's initramfs. The QEMU digest is the pinned emulator the
# flake builds, which carries `poc/time-model/qemu-nosleep-replay-flush.patch`;
# re-measuring the pinned model against it reproduced every value below, so the
# patch does not move the guest's measurements. The QEMU digest rather than only its version
# string is what makes a rebuilt or patched QEMU visible: a binary with an
# unchanged version string would otherwise pass silently.
#
Expand All @@ -30,7 +33,7 @@ icount_options=shift=4,sleep=off
kernel_sha256=d4c2d110cb9b82dad81517cc2f1f3a46d0e31c8937237cf3df59eeffb3d7070b
initramfs_sha256=24d0dfffc169a4275657c61281097882447fd565ecff87eb619cc587a57e1b4c
qemu_version=QEMU emulator version 11.1.0
qemu_sha256=a26dcb363d6817495c7fdfe824f0c60e9c0e6369d90fdbfda9648d40f8dbdbd7
qemu_sha256=2c6adfd8fc39a972ebb6051f7105f10f11cc2715efd1ff0b4e3385809a0fae0d
measurement=probe sleep requested_ns=20000000 observed_ns=21950896
measurement=probe spin chunks=64 window_ns=20000000 state=13811787166479837696
measurement=probe race work=2000000 elapsed_ns=182041872 within_deadline=no
Expand Down
Loading
Loading